滴水静禅天
扫描关注滴水静禅天

扫码加微信:-)

2 JavaScript初步Part1

滴水静禅天2020-02-23杂七杂八 1248

2 JavaScript初步Part1

<<Essential  TypeScript-From Beginner to Pro >> By Adam Freeman

ISBN-13 (pbk):  978-1-4842-4978-9

Apress

应该使用TS?

TypeScriptJavaScript语言的一个超集,专注于生成可由任何JavaScript运行时执行的安全且可预测的代码。它的主要特性是静态类型,这使得熟悉c#Java等语言的程序员更容易预测使用JavaScript。在这本书中,我解释了TypeScript的功能,并描述了它提供的不同特性。

你应该知道什么

TypeScript并不是所有问题的解决方案,重要的是要知道什么时候应该使用TypeScript,什么时候会有问题。在接下来的小节中,我将描述TypeScript提供的高级特性,以及它们可能有帮助的情况。

理解TS开发者的生产力特征

TypeScript的标题特性专注于开发人员的生产力,特别是通过使用静态类型,这有助于使JavaScript类型系统更容易使用。其他生产力特性,如访问控制关键字和简洁的类构造函数语法,有助于防止常见的编码错误。

TypeScript生产力特性应用于JavaScript代码。正如第1章所演示的,TypeScript包包含一个编译器,该编译器处理TypeScript文件并生成纯JavaScript可以由JavaScript运行时(Node.js或浏览器)执行,如图2-1所示。

JavaScriptTypeScript特性的组合保留了JavaScript的许多灵活性和动态特性,同时限制了数据类型的使用,因此对大多数开发人员来说比较熟悉,也更容易预测。这也意味着项目使用打印稿仍然可以利用广泛的第三方JavaScript包可用,要么提供特定的功能(如命令行提示在第1)接受完整的应用程序开发框架(ReactAngularVue.js框架第3部分中描述)。打印稿特性可以选择性地应用,这意味着您可以只使用这些特性用于特定的项目。如果你是TypeScriptJavaScript的新手,你可能会从使用所有TypeScript特性开始。随着您经验的增加和知识深度的增加,您会发现自己在使用TypeScript时更加专注,并且只将其特性应用于代码中特别复杂或可能导致问题的部分。

理解生产力特征的局限性

有些TypeScript特性完全由编译器实现,在应用程序运行时执行的JavaScript代码中不会留下任何痕迹。其他特性是通过构建标准的JavaScript和在编译期间执行额外的检查来实现的。这意味着您经常必须了解一个特性是如何工作的,以及如何实现它以获得最佳结果,这可能会使TypeScript特性看起来不一致且晦涩难懂。更广泛地说,TypeScript增强了JavaScript,但结果仍然是JavaScript,在TypeScript项目中开发很大程度上是一个编写JavaScript代码的过程。有些开发人员采用TypeScript是因为他们想在不学习JavaScript工作原理的情况下编写web应用程序。他们看到TypeScript是由微软生产的,并假设TypeScript是用于web开发的c#Java这种假设导致了混乱和挫折有效的TypeScript需要良好的JavaScript知识,以及它的行为方式。第3章和第4章描述了为了充分利用TypeScript您需要了解的JavaScript特性,并为理解TypeScript为何是如此强大的工具提供了坚实的基础。如果您愿意理解JavaScript类型系统,那么您会发现TypeScript是一个很好的选择但是如果你不愿意花时间去精通JavaScript,那么你就不应该使用TypeScript。在没有任何JavaScript知识的情况下将TypeScript添加到项目中会使开发变得更加困难,因为您将有两组语言特性需要讨论,它们的行为都不会完全符合您的期望。

理解TS的版本特征

JavaScript经历了一段动荡的历史,但最近成为了一致的标准化和现代化努力的焦点,引入了使JavaScript更容易使用的新特性。问题是仍然有许多JavaScript运行时不支持这些现代特性,特别是老式浏览器,这将JavaScript开发限制在普遍支持的一小部分语言特性上。JavaScript可能是一门很难掌握的语言,而当那些旨在简化开发的特性无法使用时,情况就更糟了。TypeScript编译器能够将使用现代特性编写的JavaScript代码转换成符合旧版本的JavaScript语言的代码。这允许在开发期间将最新的JavaScript特性与TypeScript一起使用,同时允许旧的JavaScript运行时执行项目生成的代码。

理解版本特性的局限性

TypeScript编译器在处理大多数语言特性方面做得很好,但是有些特性对于旧的运行时来说不能有效地翻译。如果您的目标是JavaScript的最早版本,那么您会发现并不是所有的现代JavaScript特性都可以在开发期间使用,因为TypeScript编译器无法在遗留的JavaScript中表示它们。也就是说,生成遗留JavaScript代码的需求在所有项目中并不重要,因为TypeScript编译器只是扩展工具链的一部分TypeScript编译器负责应用TypeScript特性,但结果是由其他工具进一步处理的现代JavaScript代码。这种方法通常用于web应用程序开发,您将在第3部分中看到示例。

如何搭建开发环境

如果您认为TypeScript是您项目的正确选择,那么您应该熟悉在开发中使用数据类型,并理解基本的JavaScript特性。但是,如果您不理解JavaScript如何处理数据类型,也不要担心,因为我在第3章和第4章中提供了所有对理解TypeScript有用的JavaScript特性的入门知识。在本书的第3部分中,我将演示如何将TypeScript与流行的web应用程序开发框架结合使用,这些示例需要了解HTMLCSS

TypeScript开发中唯一需要的开发工具是在第1章中创建第一个应用时安装的。后面的一些章节需要额外的包,但是提供了完整的说明。如果您在第1章中成功地构建了应用程序,那么对于TypeScript开发和本书其他章节来说,您已经做好了准备。

本书的结构

这本书分为三部分,每一部分都涵盖了一系列相关的主题。第1部分,开始使用TypeScript:本书第1部分提供了开始使用TypeScript开发所需的信息。它包括第一章、本章和介绍JavaScript提供的数据类型特性的入门章节。第5章和第6章介绍了TypeScript开发工具

2部分,理解TypeScript:本书的第2部分涵盖了开发人员生产力的TypeScript特性,包括静态类型。TypeScript提供了许多不同类型的特性,我将深入描述并通过示例进行演示。

3部分,使用TypeScript创建应用程序:TypeScript本身并不使用,所以本书的第3部分向您展示了如何使用TypeScript创建web应用程序,使用三种流行的框架:ReactAngularVue.js。这些章节解释了对每个框架都有用的TypeScript特性,并演示了如何实现web应用程序开发中通常需要的任务。为了提供理解这些框架功能的基础,我还将介绍如何创建不依赖于web应用程序框架的独立web应用程序。

其他

有很多这样的例子。学习TypeScript最好的方法是通过例子,我已经尽可能多地把它们打包到这本书里了。为了最大限度地增加本书中的示例数量,我采用了一个简单的约定,以避免重复列出相同的代码或内容。当我创建一个文件时,我将显示它的全部内容,如清单2-1所示。我将文件及其文件夹的名称包含在清单s的标题中,并以粗体显示我所做的更改。

Listing 2-1 index.ts中声明一个未知值

function calculateTax(amount, format) {

    const  calcAmount = amount * 1.2;

    return format ?  `$${calcAmount.toFixed(2)}` :  calcAmount;

}

let taxValue = calculateTax(100, false);

switch (typeof taxValue) {

    case "number":

        console.log(`Number  Value:${taxValue.toFixed(2)}`);

        break;

    case "string":

        console.log(`string  Value: ${taxValue.charAt(0)}`);

        break;

    default:

        let value =  taxValue;

        console.log(`Unexpected  type for value:${value}`);

}

let newResult = calculateTax(200, false);

let myNumber = newResult;

console.log(`Number  value:${myNumber.toFixed(2)}`);

 这是第7章的清单,其中显示了一个名为index的文件的内容。可以在src文件夹中找到。不要担心清单的内容或文件的目的;请注意,这种类型的清单包含文件的完整内容,您需要按照示例所做的更改以粗体显示。

当我所描述的特性只需要很小的更改时,有些文件可能会很长。我没有列出完整的文件,而是使用省略号(三个周期串联)来表示部分清单,它只显示文件的一部分,如清单2-2所示。

Listing 2-2 reactapp目录中的package.json配置

准备本章

有效的TypeScript开发需要了解JavaScript如何处理数据类型。这可能会让采用TypeScript的开发人员感到失望,因为他们发现JavaScript让人困惑,但是理解JavaScript使理解TypeScript变得更容易,并提供了关于TypeScript提供了什么以及它的特性如何工作的有价值的见解。在本章中,我将介绍基本的JavaScript类型特性,并在第4章中继续介绍更高级的特性。

要安装一个在JavaScript文件内容更改时自动执行该文件的包,请在primer文件夹中运行清单3-2中所示的命令。

安装nodemon(javaScript 监控器):npm install nodemon@1.18.10

这个名为nodemon的软件包将被下载并安装。安装完成后,在primer文件夹中创建一个名为index.js的文件,其内容如清单3-3所示。

tsc

npx nodemon  ./dist/index.js

我已经突出显示了index.js文件的输出部分。要确保正确地检测到更改,请更改index.js文件的内容,如清单3-5所示。

let hatPrice = 100;

console.log(`Hat price:${hatPrice}`);

 

let bootsPrice= 100;

console.log(`Boots price:${bootsPrice}`);

 

JS的迷惑性

JavaScript有许多类似于其他编程语言的特性,开发人员往往从类似清单3-5中的语句的代码开始。即使您是JavaScript新手,清单3-5中的语句也很熟悉。

JavaScript代码的构建块是语句,它们按照定义的顺序执行。let关键字用于定义变量(与定义常量值的const关键字相反)和名称。变量的值是使用赋值运算符(等号)和一个值来设置的。

JavaScript提供了一些内置对象来执行常见任务,比如使用console.log方法向命令提示符写入字符串。字符串可以定义为文字值,使用单引号或双引号,或者作为模板字符串,使用反引号字符,并使用美元符号和大括号将表达式插入模板。

但在某些时候,意想不到的结果出现了。造成这种混乱的原因是JavaScript处理类型的方式。清单3-6显示了一个典型的问题。

List 3-6 index.js中添加如下语句

let hatPrice = 100;

console.log(`Hat price:${hatPrice}`);

 

let bootsPrice = "100";

console.log(`Boots price:${bootsPrice}`);

 

if (hatPrice === bootsPrice) {

    console.log("The Price is  same.");

} else {

    console.log("The Price is  not same.");

}

 

let totalPrice = hatPrice + bootsPrice;

console.log(`Total  Price: ${totalPrice}`);

大多数开发人员会注意到,hatPrice的值是用数字表示的,而bootsPrice的值是用双引号括起来的字符串。但在大多数语言中,对不同类型执行操作都是错误的。JavaScript是不同的;比较一个字符串和一个数字会成功,但是合计这些值实际上会将它们连接起来。理解清单3-6的结果及其背后的原因可以揭示JavaScript如何处理数据类型以及TypeScript为何如此有用的细节。

理解JS的数据类型

似乎JavaScript没有数据类型或者数据类型的使用不一致,但事实并非如此。JavaScript的工作方式与大多数流行的编程语言不同,而且它的行为似乎不一致,直到您知道将会发生什么。JavaScript语言的基础是一组内置类型,如表3-1所示。

类型名

描述

number

此类型用于表示数值。与其他编程语言不同JavaScript不区分整数和浮点值,这两种值都可以用这种类型表示。

string

此类型用于表示文本数据。

boolean

这种类型可以有真值和假值

symbol

此类型用于表示唯一的常量值,如集合中的键。

null

此类型只能赋值null,并用于指示不存在无效的引用

undefined

此类型用于已定义变量但未分配值的情况。

object

此类型用于表示由单个属性和值组成的复合值。

表中的前六种类型是JavaScript基本数据类型。基本类型总是可用的,JavaScript应用程序中的每个值要么是基本类型本身,要么是由基本类型组成的。第六种类型是object,用于表示对象。

使用基础(Primitive)数据类型

如果回头看看清单3-6,就会发现代码中没有声明类型。在其他语言中,在使用变量之前需要声明它的数据类型,就像我的c#书中的这段代码一样:

string name=”Adam”; //C# style

此语句指定name变量的类型为字符串,并将其赋值为Adam。在JavaScript中,值有类型,而不是变量。要定义包含字符串的变量,需要分配一个字符串值,如清单3-7所示。

let myVariable = "Adam";

JavaScript运行时只需找出表3-1中的哪些类型应该用于分配给myVariable的值。JavaScript支持的少量类型使这个过程更简单,而且运行时知道双引号括起来的任何值都必须是字符串。您可以使用typeof关键字来确认值的类型,如清单3-8所示。

console.log(`Type: ${typeof(myVariable)}`);

myVariable = null;

console.log(`Type:${typeofmyVariable}`);

变化值赋给一个变量变化类型typeof关键词因为值报告的类型。最初的类型值分配给myVariable是字符串,然后是变量被分配一个undefined。这种动态的方法类型的有限范围内变得更加容易,JavaScript支持,这使得它更容易确定哪些内置类型的使用。例如,所有的数字所代表的数字类型,这意味着整数和浮点值都使用数字处理,这将不可能更复杂的类型。

理解空(null)的类型

typeof关键字用于空值时,结果是object。这是一种长期存在的行为,可以追溯到JavaScript最早的日子,但是由于已经编写了太多期望这种行为的代码,所以这种行为并没有改变.

理解类型强制

当操作符应用于不同类型的值时,JavaScript运行时将一个值转换为另一种类型的等效值,这个过程称为类型强制。正是类型强制特性(也称为类型转换)导致了清单3-6中的结果不一致,尽管您将了解到,一旦您理解了该特性的工作方式,结果就不是不一致的。清单3-6中的代码中有两点强制类型。

双等号使用类型强制执行比较,以便JavaScript尝试转换它正在处理的值,以生成有用的结果。这称为JavaScript抽象相等比较,当一个数字与一个字符串进行比较时,将字符串值转换为一个数字值,然后执行比较。这意味着当数字值100与字符串值100进行比较时,字符串被转换为数字值100,这就是if表达式计算结果为true的原因。

清单3-6中第二次使用强制转换是在价格加总时。

当您对数字和字符串使用+运算符时,将转换其中一个值。令人困惑的是,转换和比较是不一样的。如果其中一个值是字符串,则将另一个值转换为字符串,并将两个字符串值连接起来。这意味着当数字值100添加到字符串值100时,该数字被转换为字符串并连接起来生成字符串结果100100

避免无意的类型强制

类型强制可能是一个有用的特性,它之所以名声不好,只是因为它是在无意中应用的,而当正在处理的类型被更改为新值时,很容易发生这种情况。您将在后面的章节中了解到,TypeScript提供了一些特性来帮助管理不必要的强制,并确保仅在显式选择时才使用它。但是JavaScript也提供了一些特性来防止强制,如清单3-10所示。

Listing 3-10 避免类型强制陷阱

let hatPrice = 100;

console.log(`Hat price:${hatPrice}`);

 

let bootsPrice = "100";

console.log(`Boots price:${bootsPrice}`);

 

if (hatPrice ===  bootsPrice) {

    console.log("The Price is  same.");

} else {

    console.log("The Price is  not same.");

}

 

let totalPrice = Number(hatPrice)  +Number( bootsPrice);

console.log(`Total Price: ${totalPrice}`);

 

let myVariable = "Adam";

console.log(`Type: ${typeof  (myVariable)}`);

myVariable = null;

console.log(`Type:${typeof  myVariable}`);

双等号(==)执行应用类型强制的比较。三重等号(===)应用了一个严格的比较,只有当值具有相同的类型且相等时才会返回true。为了防止字符串串接,可以在使用内置的Number函数应用+运算符之前显式地将值转换为数字,其效果是执行数字加法。清单3-10中的代码产生以下输出:

应用类型约束的价值

当显式应用类型强制时,类型强制可能是一个有用的特性。一个有用的特性是通过逻辑或操作符(||)将值强制转换成布尔类型。将null或未定义的值转换为false值,这是提供回退值的有效工具,如清单3-11所示。

Listing 3-11 处理Null

let firstCity;

let secondCity = firstCity || "London";

console.log(`City:${secondCity}`);

名为secondCity的变量的值是用一个检查firstCity值的表达式设置的:如果firstCity转换为布尔值true,那么secondCity的值将是firstCity的值。未定义的类型用于定义了变量但没有为其赋值的情况,这就是名为firstCity的变量的情况,使用||操作符可以确保在firstCity未定义或为null时使用secondCity的回退值

使用函数

JavaScript用于类型的灵活方法在该语言的其他部分(包括函数)中也得到了遵循。清单3-12为示例JavaScript文件添加了一个函数,并删除了前面示例中的一些语句,以保持简洁。

Listing 3-12 定义一个函数

//定义一个函数

function sumPrices(first, second, third) {

    return first +  second + third;

}

//使用函数

let totalPrice1 = sumPrices(hatPrice,  bootsPrice);

console.log(`Total  Price: ${totalPrice1}`);

函数参数的类型由用于调用它的值决定。例如,一个函数可以假设它将接收数值,但是没有任何东西可以阻止使用字符串、布尔值或对象参数来调用函数。如果函数没有认真验证它的假设,可能会产生意外的结果,这可能是因为JavaScript运行时强制了值,也可能是因为使用了特定于单一类型的特性。清单3-12中的sumPrices函数使用了+运算符,用于对一组数字参数求和,但是用于调用该函数的一个值是字符串,正如本章前面所解释的,应用于字符串值的+运算符执行连接。清单3-12中的代码产生以下输出:

JavaScript不会强制一个函数定义的参数数量与调用它的参数数量相匹配没有提供值的任何参数都是undefined。在清单中,没有为名为third的参数提供值,未定义的值被转换为未定义的字符串值,并包含在连接输出中。

使用函数结果

JavaScript类型与其他语言之间的差异被函数放大了JavaScript类型特性的一个结果是,用于调用函数的参数可以确定结果的类型,如清单3-13所示。

//定义一个函数

function sumPrices(first, second, third) {

    return first +  second + third;

}

//使用函数

totalPrice = sumPrices(hatPrice, bootsPrice);

console.log(`Total Price: ${totalPrice} ${typeof  totalPrice}`);

 

totalPrice = sumPrices(100, 200, 300);

console.log(`Total Price: ${totalPrice} ${typeof  totalPrice}`);

 

totalPrice = sumPrices(100, 200);

console.log(`Total  Price: ${totalPrice} ${typeof  totalPrice}`);

通过调用sumPrices函数,将totalPrice变量的值设置三次。在每个函数调用之后,typeof关键字用于确定函数返回的值的类型。清单3-13中的代码产生以下输出.

第一个函数调用包含一个字符串参数,该参数将导致所有函s参数转换为字符串值并连接起来,这意味着该函数返回字符串值100100undefined。第二个函数调用使用三个数字值,它们相加得到数字结果600。最后一个函数调用使用了number参数,但是没有提供第三个值,这导致了一个未定义的参数。JavaScript将未定义的值合并为特殊的数字值NaN(表示不是数字)。包含NaN的加法的结果是NaN,这意味着结果的类型是number,但是值没有用处,不太可能是我们想要的。

避免参数误配问题

尽管前一节中的结果可能会引起混淆,但它们是JavaScript规范中描述的结果。问题不在于JavaScript是不可预测的,而在于它的方法不同于其他流行的编程语言

JavaScript提供的特性可用于避免以下部分中的问题。第一个参数是默认的参数值,如果在调用函数时没有相应的参数,则使用这些参数值,如清单3-14所示。

Listing 3-14 改进1

List 3-15 改进2

//定义一个函数

function sumPrices(...numbers//rest参数) {

    return numbers.reduce(

        function (total,  val) { return total + val }

        , 0);

}

rest参数是一个包含所有未定义参数的参数的数组。清单3-15中的函数只定义了一个rest参数,这意味着它的值将是一个包含用于调用函数的所有参数的数组。数组的内容使用内置的数组缩减方法进行求和JavaScript数组在处理数组一节中进行了描述,并使用reduce方法为数组中的每个对象调用一个函数来生成单个结果值。这种方法确保参数的数量不会影响结果,但是reduce方法调用的函数使用加法运算符,这意味着字符串值仍然会被连接起来。清单生成以下输出.

Listing 3-16 改进3

 

这个Number.isNaN方法用于检查数字值是否为NaN,清单3-16中的代码显式地将每个参数转换为数字,并将NaN的值替换为0。只处理可视为数字的参数值,而添加到最终函数调用的未定义、布尔型和字符串参数对结果没有影响。

使用箭头函数(Lambda表达式)

箭头函数也称为胖箭头函数lambda表达式,它们是另一种简洁定义函数的方法,通常用于定义作为其他函数参数的函数。清单3-17将数组reduce方法使用的标准函数替换为一个箭头函数。

Listing 3-17 使用箭头函数改进

//定义一个函数

function sumPrices(...numbers) {

    return  numbers.reduce((total, val) =>

        total +  (Number.isNaN(Number(val)) ? 0 : Number(val))

    );

}

箭头函数由三部分组成:输入参数,然后是等号和大于号(箭头)最后是结果值。仅当箭头函数需要执行多个语句时,才需要return关键字和大括号。箭头函数可以在任何需要函数的地方使用,它们的使用取决于个人的偏好,但在理解这个关键字部分中描述的问题除外。清单3-18在箭头语法中重新定义了sumPrices函数。

Listing 3-18 使用函数编程(functional programming)风格

let  sumPrices = (...numbers) => numbers.reduce((total, val) => total +  (Number.isNaN(Number(val)) ? 0 : Number(val)));

不管使用哪种语法,函数都是值。它们是对象类型的一个特殊类别,在使用对象一节中进行了描述,并且可以将函数分配给作为参数传递给其他函数并像使用其他值一样使用的变量。在清单3-18中,箭头语法用于定义一个函数,该函数被分配一个名为sumPrices的变量。函数是特殊的,因为它们可以被调用,但是能够将函数作为值来处理使得复杂的功能可以被简明地表达出来,尽管创建难于阅读的代码很容易。还有更多的例子,箭头函数和使用函数作为值在整本书。

使用数组

JavaScript数组遵循大多数编程语言所采用的方法,只是它们的大小是动态调整的,并且可以包含任何值的组合,因此也可以包含任何类型的组合。清单3-19展示了如何定义和使用数组。

Listing 3-19 数组的使用

let names = ["Hat", "Boots", "Gloves"];

let prices = [];

 

prices.push(100);

prices.push("100");

prices.push(50.25);

 

console.log(`First Item:${names[0]}:${prices[0]}`);

 

let totalPrice1 = sumPrices(...prices);

console.log(`Total:${totalPrice1} ${typeof  totalPrice1}`);

数组的大小在创建时未指定,将在添加或删除项时自动分配。JavaScript数组是从零开始的,使用方括号定义,初始内容可以用逗号分隔。示例中的names数组是用三个字符串值创建的。创建的price数组为空,使用push方法将项追加到数组的末尾。可以使用方括号读取或设置数组中的元素,也可以使用表3-2中描述的方法进行处理。

常用数组方法

方法

描述

concat(其他数组)

此方法返回一个新数组,该数组将调用它的数组与指定为参数的数组连接起来。可以指定多个数组。

join(分隔符)

该方法将数组中的所有元素连接起来形成一个字符串。参数指定用于分隔项的字符。

pop()

推出最后一项.

shift()

此方法删除并返回数组中的第一个元素

push(item)

压栈

unshift(item)

该方法在数组的开头插入一个新项

reverse()

逆序输出

slice(start,end)

数组提取

every(test)

此方法为数组中的每个项调用测试函数,如果函数为所有项返回true,则返回true,否则返回false

some(test)

如果为数组中的每个项调用测试函数至少一次返回true,则此方法将返回true

filter(test)

此方法返回一个新数组,其中包含测试函数返回true的项。

find(test)

此方法返回测试函数返回true的数组中的第一项。

findIndex(test)

此方法返回测试函数返回true的数组中第一项的索引。

forEach(回调)

如前一节所述,此方法为数组中的每个项调用回调函数。

includes(value)

如果数组包含指定的值,则此方法返回true

map(回调)

此方法返回一个新数组,其中包含为数组中的每个项调用回调函数的结果。

reduce(回调)

此方法返回通过为数组中的每个项调用回调函数生成的累积值。

 

使用数据的传播算符(Spread)

扩展运算符可用于展开数组的内容,以便将其元素用作函数的参数。spread操作符是三个句点(),在清单3-19中用于将数组的内容传递给sumPrices函数。

运算符在数组名之前使用。扩展操作符还可以用于展开数组的内容,以便进行简单的连接,如清单3-20所示。

let combinedArray = [...names, ...prices];

combinedArray.forEach(element=>console.log(`Combined  Array Element:${element}`));

使用对象

JavaScript对象是属性的集合,每个属性都有一个名称和一个值。定义对象的最简单方法是使用文字语法,如清单3-21所示。

Listing 3-21 创建对象示例

let hat = { name: "Hat", price:  100 };

let boots = { name: "Boots", price: "100" };

 

let sumPrices = (...numbers) =>  numbers.reduce((total, val) => total + (Number.isNaN(Number(val)) ? 0 :  Number(val)));

 

let totalPrice = sumPrices(hat.price, boots.price);

console.log(`Total  :${totalPrice} ${typeof  totalPrice}`);

Total :200 number

对象性质的增加,改变和删除

JavaScript的其他部分一样,对象也是动态的。可以添加和删除属性,任何类型的值都可以分配给属性,如清单3-22所示。

Listing 3-22 操作对象

let hat = { name: "Hat", price:  100 };

let boots = { name: "Boots", price: "100" };

let gloves = { productName: "Gloves", price: "40" };

gloves.name = gloves.productName;

delete gloves.productName;

gloves.price = 20;

 

let sumPrices = (...numbers) =>  numbers.reduce((total, val) => total + (Number.isNaN(Number(val)) ? 0 : Number(val)));

 

let totalPrice = sumPrices(hat.price,  boots.price,gloves.price);

console.log(`Total :${totalPrice} ${typeof  totalPrice}`);

 

防止未定义的对象和属性

在使用对象时需要小心,因为它们可能没有您期望的形状(用于属性和值的组合的术语),或者在创建对象时最初使用的形状。因为对象的形状可以更改,所以设置或获取未定义的属性值不是错误。如果您设置了一个不存在的属性,那么它将被添加到对象并分配指定的值。如果您读取了一个不存在的属性,那么您将接收到未定义的属性。确保代码始终具有要处理的值的一个有用方法是依赖类型强制特性和逻辑OR操作符,如清单3-23所示。

Listing 3-23 防止未定义的值

let hat = { name: "Hat", price:  100 };

let boots = { name: "Boots", price: "100" };

let gloves = { productName: "Gloves", price: "40" };

 

gloves.name = gloves.productName;

delete gloves.productName;

gloves.price  = 20;

 

let propertyCheck = hat.price || 0;

let objectAndPropertyCheck = (hat || {}).price  || 0;

console.log(`Checks:  ${propertyCheck},${objectAndPropertyCheck}`);

 

对象的SpreadRest操作

spread操作符可用于展开一个对象定义的属性和值,这使得根据另一个对象定义的属性创建一个对象变得很容易,如清单3-24所示。

let otherHat = { ...hat };

console.log(`Spread:  ${otherHat.name},${otherHat.price}`);

Spread: Hat,100

扩展操作符还可以与其他属性组合,以添加、替换或吸收源对象中的属性,如清单3-25所示。

let hat = {

    name: "Hat",

    price: 100

};

 

let boots = {

    name: "Boots",

    price: "100"

};

 

let additionalProperties = { ...hat,  discounted: true };

console.log(`Additional:${JSON.stringify(additionalProperties)}`);

 

let replacedProperties = { ...hat, price: 10  };

console.log(`Replaced:${JSON.stringify(replacedProperties)}`);

 

let { price, ...somePro } = hat;

console.log(`Selected:${JSON.stringify(somePro)}`);

 

定义GettersSetters

gettersetter是在读取或分配属性值时调用的函数,如清单3-26所示

Listing 3-26 GettersSetters

/*Getters and Setters*/

 

let hat = {

    name: "Hat",

    _price: 100,

    priceIncTax: 100 * 1.2,

 

    set  price(newPrice) {

        this._price =  newPrice;

        this.priceIncTax  = this._price * 1.2;

    },

 

    get price()  {

        return this._price;

    }

};

 

let boots = {

    name: "Boots",

    price: "100",

 

    get  priceIncTax() {

        return Number(this.price) *  1.2;

    }

};

 

console.log(`Hat:${hat.price},${hat.priceIncTax}`);

hat.price = 120;

console.log(`Hat:${hat.price},${hat.priceIncTax}`);

 

console.log(`Boots:${boots.price},${boots.priceIncTax}`);

boots.price = "120";

console.log(`Boots:${boots.price},${boots.priceIncTax}`);

这个例子引入了一个priceIncTax属性,它的值在设置price属性时自动更新。当一个新值被分配给price属性时,setter将更新后台属性和priceIncTax属性。当读取price属性的值时,getter将使用_price属性的值进行响应。(需要一个支持属性,因为gettersetter被视为属性,不能与对象定义的任何常规属性具有相同的名称。)了解JAVASCRIPT私有属性.

理解Javascript的私有属性

JavaScript没有任何对私有属性的内置支持,这意味着只能通过对象的方法、gettersetter来访问属性。有一些技术可以达到类似的效果,但是它们很复杂,因此最常见的方法是使用命名约定来表示不打算供公众使用的属性。这并不会阻止对这些属性的访问,但至少可以清楚地看到这样做是不可取的。广泛使用的命名约定是在属性名前面加上下划线,如清单3-26中的_price属性所示。在我写这篇文章时,有一个提案正在通过标准化过程向JavaScript语言添加对私有属性的支持。私有属性的名称将以#字符作为前缀,但是这个特性成为JavaScript标准的一部分还需要一段时间。typeScript提供私有属性,如第11章所述。

定义方法

JavaScript一开始可能会让人感到困惑,但深入研究细节就会发现,它的一致性在日常使用中并不总是很明显。方法就是一个例子,它建立在前面几节描述的特性之上,如清单3-27所示。

Listing 3-27 定义方法

let hat = {

    name: "Hat",

    _price: 100,

    priceIncTax: 100 * 1.2,

 

    set  price(newPrice) {

        this._price =  newPrice;

        this.priceIncTax  = this._price * 1.4;

    },

 

    get price()  {

        return this._price;

    },

    writeDetails: function () {

        console.log(`${this.name}:${this.price},${this.priceIncTax}`);

    }

};

 

hat.writeDetails();

hat.price = 120;

hat.writeDetails();

方法是一个属性,其值是一个函数,这意味着函数提供的所有特性和行为,如defaultrest参数,都可以用于方法。清单3-27中的方法是使用function关键字定义的,但是可以使用更简洁的语法

理解this关键字

即使是经验丰富的JavaScript程序员也会对这个关键字感到困惑。在其他编程语言中,这用于引用从类创建的对象的当前实例。在JavaScript中,这个关键字常常以相同的方式出现,直到应用程序发生更改,未定义的值开始出现。

为了演示,我使用了fat箭头语法来重新定义hat对象上的方法,如清单3-29所示。

Listing 3-29 writeDetails字段使用箭头语法

    writeDetails: () => console.log(`${this.name}:${this.price},${this.priceIncTax}`)

 

hat.writeDetails();

hat.price = 120;

hat.writeDetails();

该方法使用与清单3-28相同的console.log语句,但是当保存更改并执行代码时,输出将显示未定义的值,如下所示

要理解为什么会发生这种情况并能够修复问题,需要后退一步并检查this关键字在JavaScript中的实际作用

理解独立函数中的this关键字

this关键字可以用于任何函数,即使该函数没有用作方法,如清单3-30所示。

Listing 3-30 调用一个函数

function writeMessage(message) {

    console.log(`${this.greeting},${message}`);

}

 

greeting = "Hello";

 

writeMessage("Is  is sunny today!");

writeMessage函数从传递给console.log方法的模板字符串中的一个表达式中读取名为this .greeting的属性关键字不会再次出现在清单中,但是当代码被保存并执行时,将产生以下输出

Hello, It is sunny today

JavaScript定义了一个全局对象,可以为其分配在整个应用程序中可用的值。全局对象用于提供对执行环境中的基本特性的访问,比如允许与文档对象模型API交互的浏览器中的文档对象。

没有使用letconstvar关键字的赋值名称被分配给全局对象。赋值字符串值Hello的语句在全局范围内创建一个变量。当函数执行时,它被分配给全局对象,所以读取this.greeting返回字符串值Hello,解释应用程序生成的输出。调用函数的标准方法是使用包含参数的括号,但是在JavaScript中,这是一种方便的语法,可以转换成清单3-31所示的语句。

writeMessage.call(global,  "It is sunny day!");

如前所述,函数是对象,这意味着它们定义方法,包括call方法。这个方法用于在幕后调用一个函数。调用方法的第一个参数是this的值,它被设置为全局对象。这就是它可以用于任何函数的原因,也是它默认返回全局对象的原因。

清单3-31中的新语句直接使用call方法,并将this值设置为全局对象,其结果与之前的常规函数调用相同,可以在以下执行时代码生成的输出中看到:

全局对象的名称会根据执行环境而更改。在节点执行的代码中。使用的是全局变量,但在浏览器中需要使用windowself。在撰写本文时,有一个建议将名称global标准化,但它还没有被作为JavaScript规范的一部分采用。

理解严格模式(strict mode)

JavaScript支持严格的模式,禁用限制功能,历史上造成低质量的软件,或者有效地防止运行时执行代码。当启用严格模式时,this的默认值是undefined,以防止意外使用全局对象,全球范围的值必须明确定义为全局对象的属性。有关详细信息,请参阅https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode。打印稿编译器提供了一个特性来自动启用严格模式在它生成的JavaScript代码,如第五章所述。

理解方法中的this

当一个函数被调用为对象的方法时,它被设置为对象,如清单3-32所示

let myObject = {

    greeting: "Hi,  there",

    writeMessage(message) {

&nbspnbsp;       console.log(`${this.greeting}, ${message}`);

    }

}

 

greeting = "Hello";

myObject.writeMessage("It  is a Sunny Today!");

需要注意的是,如果在对象之外访问函数,则设置会有所不同,如果将函数赋给一个变量,则会发生这种情况,如清单3-33所示。

let myFunction = myObject.writeMessage;

myFunction("It  is sunny today");

函数可以像其他值一样使用,包括将它们分配给定义它们的对象之外的变量,如清单所示。如果函数是通过变量调用的,那么它将被设置为全局对象。当函数被用作其他方法的参数或处理事件的回调函数时,这通常会导致问题,结果是相同的函数会根据调用方式而有不同的行为,如清单3-33中的代码生成的输出所示。

改变this关键字的行为

控制这个值的一种方法是使用call方法来调用函数,但是这很麻烦,每次调用函数时都必须这样做。更可靠的方法是使用函数的 bind方法,该方法用于设置函数的值,无论如何调用函数,如清单3-34所示。

bind方法将返回一个新函数,该函数在被调用时将对此具有一个持久值bind方法返回的函数用于替换原始方法,以确保在调用writeMessage方法时保持一致性。使用bind很麻烦,因为对对象的引用在创建之后才可用,这导致创建对象然后调用bind来替换每个需要一致的这个值的方法的两个步骤。清单3-34中的代码产生以下输出

理解箭头函数中的this

更复杂的是,箭头函数的工作方式与常规函数不同Arrow函数没有自己的this,而是在执行时继承它所能找到的最接近的值。为了演示这是如何工作的,清单3-35在示例中添加了一个arrow函数。

let myObject = {

    greeting: "Hi,  there",

    getWriter() {

        return  (message) => console.log(`${this.greeting}, ${message}`);

    }

}

greeting = "Hello";

let writer = myObject.getWriter();

writer("It is raining today");

let standAlone = myObject.getWriter;

let standAloneWriter = standAlone();

standAloneWriter("It  is sunny today");

返回到原始的问题

在本节开始时,我在箭头语法中重新定义了一个函数,并展示了它的不同行为,在其输出中产生了undefined。这是对象和它的函数.

由于arrow函数没有自己的这个值,而且arrow函数不是由一个可以提供该值的常规函数所封装的,所以行为发生了变化。为了解决这个问题并确保结果是一致的,我必须返回到一个常规函数并使用bind方法来修复这个值,如清单3-36所示。

let hat = {

    name: "Hat",

    _price: 100,

    priceIncTax: 100 * 1.2,

 

    set  price(newPrice) {

        this._price =  newPrice;

        this.priceIncTax  = this._price * 1.2;

    },

 

    get price()  { return this._price },

 

    writeDetails() {

        console.log(`${this.name}:${this.price},${this.priceIncTax}`);

    }

};

 

let boots = {

    name: "Boots",

    price: "100",

 

    get  priceIncTax() {

        return Number(this.price) *  1.2;

    }

}

 

hat.writeDetails = hat.writeDetails.bind(hat);

hat.writeDetails();

hat.price = 120;

hat.writeDetails();

 

console.log(`Boots: ${boots.price}, ${boots.priceIncTax}`);

boots.price = "120";

console.log(`Boots:  ${boots.price}, ${boots.priceIncTax}`);

 

总结

在本章中,我介绍了JavaScript类型系统的基本特性。这些特性常常引起混淆,因为它们的工作方式与其他编程语言不同。理解这些特性使使用TypeScript变得更容易,因为它们提供了对TypeScript解决的问题的深入了解。在下一章中,我将描述更多对理解TypeScript有用的JavaScript类型特性。

发表评论