本文章为实体书内容
第一章 什么是JavaScript
小结:
JavaScript是一门用来与网页交互的脚本语言,包含以下三个组成部分。
- ECMAScript:由ECMA-262定义并提供核心功能。
- 文档对象模型(DOM):提供与网页内容交互的方法与接口。
- 浏览器对象模型(BOM):提供与浏览器交互的方法与接口。
所有浏览器基本上对ES5(ECMAScript5)提供了完善的支持,而对ES6和ES7的支持度也在不断提升。
第二章 HTML中的JavaScript
使用外部JavaScript文件:
- 可维护性。可独立于使用JavaScript的HTML页面来维护代码。
- 缓存。如果两个页面都用到同一个文件,则该文件只需要下载一次。
- 适应未来。不必考虑XHTML。
小结:
JavaScript是通过
- 要包含外部JavaScript文件,必须将是src属性设置为要包含文件的URL。文件可以跟网页在同一台服务器上,也可以位于完全不同的域。
- 所有
- 对于不推迟执行的脚本,浏览器必须解释完位于标签之前。
- 可以使用defer属性把脚本推迟到文档渲染完毕后在执行。推迟的脚本总是按照它们被列出的次序执行。
- 可以使用async属性表示脚本不需要等待其他脚本,同时也不阻塞文档渲染,即异步加载。异步脚本不能保证按照它们在页面中出现的次序执行。
- 通过使用元素,可以指定在浏览器不支持脚本时显示的内容。如果浏览器支持并启用脚本,则元素中的任何内容都不会被渲染。
第三章 语言基础
3.1 语法
首先要知道的是,ECMAScript中一切都区分大小写。
3.2 关键字与保留字
ECMA-262第6版规定的所有关键字如下:
break do in typeof case else instanceof var catch export new void class extends return while const finally super with continue for switch yield debugger function this default if throw delete import try
3.3 变量
3.3.1 var关键字
1.var声明作用域
使用var操作符定义的变量会成为包含它的函数的局部变量。比如,使用var在一个函数内部定义一个变量,就意味着该变量将在函数退出时被销毁:
function test() {
var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错
在函数内定义变量时省略var操作符,可以创建一个全局变量:
function test() {
message = "hi"; // 全局变量
}
test();
console.log(message); // "hi"
2.var声明提升
使用var时,下面的代码不会报错。是因为使用这个关键字的声明的变量会自动提升到函数作用域顶部:
function foo() {
console(age);
var age = 26;
}
foo(); // undefind
之所以不会报错,是因为ECMAScript运行时把它看成等价于如下代码:
function foo() {
var age;
console(age);
var age = 26;
}
foo(); // undefind
这就是所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部。此外反复多次使用var声明同一个变量也没有问题。
3.3.2 let声明
let和var的作用差不多,但有着非常重要的区别。最明显的是,let声明的范围是块作用域,而var声明的是函数作用域。
if (true) {
var name = 'Matt';
console.log(name); // Matt
}
console.log(name); // Matt
if (true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age 没有定义
在这里,age变量之所以不能在 if 块外部被引用,是因为它的作用域仅限于该区块内部。块作用域是函数作用域的子集,因此适用于 var 的作用域限制同样适用于 let。
let 也不允许同一个区块作用域中出现冗余声明。这样会导致报错:
var name;
var name;
let age;
let age; // SyntaxError; 标识符age已经声明过了。
1.暂时性死区
let 和 var 的另一个重要的区别,就是 let 声明的变量不会在作用域中被提升。
// name 会被提升
console.log(name); // undefined
var name = 'Matt';
// age不会被提升
console.log(age); // ReferenceError: age 没有定义
let age = 26;
在解析代码时,JavaScript引擎也会注意出现在块后面的let声明,只不过在此之前不能以任何方式来引用未声明的变量。在let声明之前的执行瞬间被称为 “暂时性死区”(temporal dead zone),在此阶段引用任何后面才声明的变量都会抛出ReferenceError。
2.全局声明
与var关键字不同,使用 let 在全局作用域中声明的变量不会成为 window 对象的属性(var声明的变量则会)。
var name = 'Matt';
console.log(window.name); // 'Matt'
let age = 26;
console.log(window.age); // undefined
不过,let 声明仍然实在全局作用域中发生的,相应的变量会在页面的生命周期内存续。因此,为了避免 SyntaxError,必须确保页面不会重复声明同一个变量。
3.条件声明
不能使用 let 进行条件式声明。
4.for 循环中的 let 声明
在let出现之前,for 循环定义的迭代变量会渗透到循环体外部:
for (var i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // 5
改成使用 let 之后,这个问题就消失了,因为迭代变量的作用域仅限于 for 循环块内部:
for (let i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // ReferenceError: i 没有定义
在使用 var 的时候,最常见的问题就是对迭代变量的奇特声明和修改:
for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 你可能以为会输出0、1、2、3、4
// 实际上会输出 5、5、5、5、5
之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值:5.在之后执行超时逻辑时。所有的 i 都是同一个变量,因而输出的都是同一个最终值。
而在使用 let 声明迭代变量时,JavaScript 引擎在后台会为每个迭代循环声明一个新的迭代变量。每个 setTimeout 引用的都是不同的变量实例,所以 console.log 输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。
for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 会输出 0、1、2、3、4
3.3.3 const声明
const 的行为与 let 基本相同,唯一一个重要的而区别是用它声明变量时同时初始化变量,且尝试修改const声明的变量会导致运行时错误。
const age = 26;
age = 36; // TypeError:给常量赋值
// const 也不允许重复声明
const name = 'Matt';
const name = 'Nicholas'; // SyntaxError
//const 声明的作用域也是块
const name = 'Matt';
if (true) {
const name = 'Nicholas';
}
console.log(name); // Matt
const 声明的限制只适用于它指向的变量的引用。换句话说,如果 const 变量引用的是一个对象,那么修改这个对象内部的属性并不违反 const 的限制。
3.3.4 声明风格及最佳实践
1.不使用var
限制自己只使用 let 和 const 有助于提升代码质量,因为变量有了明确的作用域、声明位置,以及不变的值。
2.const优先,let次之
使用 const 声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。因此,很多开发者认为应该优先使用 const 来声明变量,旨在 提前知道未来有修改时,再使用 let 。这样可以让开发者更有信心的推断某些变量的值永远不会变,同时也能迅速发现因意外赋值导致的非预期行为。
3.4数据类型
小结:
ECMAScript中的基本元素:
- ECMAScipt 中的基本数据类型包括Undefined、Null、Boolean、Number、String 和 Symbol。
- 与其他语言不同,ECMAScript 不区分整数和浮点值,只有 Number 一种数据类型。
- Object 是一种复杂数据类型,它是这门语言中所有对象的基类。
- 严格模式为这门语言中某些容易出错的部分施加了限制。
- ECMAScript 提供了 C 语言和类 C 语言中常见的很多基本操作符,包括数学操作符、布尔操作符、关系操作符、相等操作符和赋值操作符等。
- 这门语言中的流程控制语句大多是从其他语言中借鉴过来的,比如 if 语句、for 语句和 switch 语句等。
ECMAScript 中的函数与其他语言中的函数不一样。
- 不需要指定函数的返回值,因为任何函数可以在任何时候返回任何值。
- 不指定返回值的函数实际上会返回特殊值 undefined。
第四章 变量、作用域与内存
4.1原始值和引用值
ECMAScript 变量可以包含两种不同类型的数据:原始值和引用值。原始值 ( primitive value ) 就是最简单的数据,引用值 (reference value ) 则是由多个值构成的对象。
保存原始值的变量是按值( by value )访问的,因为我们操作的就是储存在变量中的实际值。
在操作对象时,实际操作的是对该对象的引用( reference )而非实际对象本身。为此,保存引用值的变量是按引用( by reference )访问的。
4.1.1 动态属性
let person = new object();
person.name = "Nicholas";
console.log(person,name); // "Nicholas"
这里,首先创建了一个对象,并把它保存在变量 person 中。然后,给这个对象添加了一个名为 name 的属性,并给这个属性赋值了一个字符串“Nicholas”。在此之后,就可以访问这个新属性,直到对象被销毁或属性被显式删除。
原始值不能有属性,尽管尝试给原始值添加属性不会报错。比如:
let name = "Nicholas";
name.age = 26;
console.log(name.age); // undefined
记住,只有引用值可以动态添加后面可以使用的属性。
注意,原始类型的初始化可以只使用原始字面量形式。如果使用的是 new 关键字,则 JavaScript 会创建一个 Object 类型的实例,但其行为类似原始值。下面来看看这两种初始化方式的差异:
let name1 = "Nicholas";
let name2 = new String("Matt");
name1.age = 27;
name2.age = 26;
console.log(name1.age); // undefined
console.log(name2.age); // 26
console.log(typeof name1); // string
console.log(typeof name2); // object
4.1.2 复制值
除了储存方式不同,原始值和引用值在通过变量复制时也有所不同。在通过变量把一个原始值赋值到另一个变量时,原始值会被复制到新变量的位置。请看下面的例子:
let num1 = 5;
let num2 = num1;
这两个变量可以独立使用,互不干扰。
在把引用值从一个变量赋值到另一个变量时,储存在变量中的值也会被复制到新变量所在的位置。区别在于,这里复制的值实际上是一个指针,它指向储存在堆内存中的对象。操作完成后,两个变量实际上指向同一个对象,因此一个对象上面的变化会在另一个对象上反映出来,如下面的例子所示:
let obj1 = new Object();
let obj2 = obj1;
obj1.name = "Nacholas";
console.log(obj2.name); // "Nicholas"
4.1.3 传递参数
ECMAScript 中所有函数的参数都是按值传递的。这意味着函数外的值会被复制到函数内部的参数中,就像一个变量复制到另一个变量一样。如果是原始值,那么就跟原始值变量的复制一样,如果是引用值,那么就跟引用值变量的复制一样。
在按值传递的时,值会被复制到一个局部变量( 即一个命名参数,或者用ECMAScript 的话说,就是 arguments 对象中的一个槽位)。在按引用传递参数时,值在内存中的位置会被保存在一个局部变量,就意味着对本地变量的修改会反映到函数外部。(这在 ECMAScript 中是不可能的。)来看这个例子:
function addTen(num) {
num += 10;
return num;
}
let count = 20;
let result = addTen(count);
console.log(count); // 20, 没有变化
console.log(result); // 30
这里,函数addTen() 有一个参数num,它其实是一个局部变量。在调用时,变量count作为参数传入。count 的值是20,这个值被复制到参数 num 以便在 addTen()内部使用。在函数内部,参数 num 的值被加上了10但这不会影响函数外部的原始变量 count。如果 num 是按引用传递的,那么 count 的值也会被修改为30。但是,如果变量中传递的是对象,就没那么清楚了。比如,再看这个例子:
function setName(obj) {
obj.name = "Nicholas";
}
let person = new Object();
setName(person);
console.log(person.name); // "Nicholas"
4.4小结
JavaScript 变量可以保存两种类型的值:原始值和引用值。原始值可能是以下 6 种原始数据之一:Underfined、Null、Boolean、Number、String 和 Symbol。原始值和引用值有以下特点。
- 原始值大小固定,因此保存在栈内存上。
- 从一个变量到另一个变量复制原始值会创建该值的第二个副本。
- 引用值是对象,储存在堆内存上。
- 包含引用值的变量实际上只包含指向相应对象的一个指针,而不是对象本身。
- 从一个变量到另一个变量复制值引用值只会复制指针,因此结果是两个变量都指向同一个对象。
- typeof 操作符可以确定值的原始类型,而 instanceof 操作符用于确保值的引用类型。
任何变量 ( 不管包含的是原始值还是引用值 ) 都存在于某个执行上下文中 ( 也称为作用域 )。这个上下文 ( 作用域 )决定了变量的生命周期,以及它们可以访问代码的哪些部分。执行上下文可以总结如下。
- 执行上下文分为全局上下文、函数上下文和块级上下文。
- 代码执行流每进入一个新上下文,都会创建一个作用域链,用于搜索变量和函数。
- 函数或块的局部上下文不仅可以访问自己作用域内的变量,而且也可以访问任何包含上下文乃至全局上下文中的变量。
- 全局上下文只能访问全局上下文中的变量和函数,不能直接访问局部上下文中的任何数据。
- 变量的执行上下文用于确定什么时候开始释放内存。
JavaScript 是使用垃圾回收的编程语言,开发者不需要操心内存分配和回收。JavaScript 的垃圾回收程序可以总结如下。
- 离开作用域的值会被自动标记为可回收,然后在垃圾回收期间被删除。
- 主流的垃圾回收算法是标记清理,即给当前不使用的值加上标记,再回来回收它们的内存。
- 引用计数是另一种垃圾回收策略,需要记录值被引用了多少次。JavaScript 引擎不再使用这种算法,但某些旧版本的 IE 仍然会受这种算法的影响,原因是 JavaScript 会访问非原生 JavaScript 对象 ( 如DOM元素 )。
- 引用计数在代码中存在循环引用时会出现问题。
- 解除全局变量的引用不仅可以消除循环引用,而且对垃圾回收也有帮助。为促进内存回收,全局对象、全局对象的属性和循环引用都应该在不需要时接触引用。
第五章 基本引用类型
5.2 RegExp
ECMAScript 通过 RegExp 类型支持正则表达式。正则表达式使用类似 Perl 的简洁语法来创建:
let expression = /pattern/flags;
这个正则表达式的 pattern ( 模式 )可以时任何简单或复杂的正则表达式,包括字符类、限定符、分组、向前查找和反向引用。每个正则表达式可以带零个或多个 flags ( 标记 ),用于控制正则表达式的行为。下面给出了表示匹配模式的标记。
- g:全局模式,表示查找字符串的全部内容,而不是找到第一个匹配的内容就结束。
- i:不区分大小写,表示在查找匹配时忽略 pattern 和字符串的大小写。
- m:多行模式,表示查找到一行文本末尾时会继续查找。
- y:黏附模式,表示只查找从 lastIndex 开始及之后的字符串。
- u:Unicode 模式,表示启用 Unicode 匹配。
- s:dotAll 模式,表示元字符 .匹配任何字符 ( 包括\n或\r)
第六章 集合引用类型
6.1 Object
在对象字面量标识法中,属性名可以是字符串或数值,比如:
let person = {
"name": "Nicholas",
"age": 29,
5: true
}
这个例子会得到一个带有属性 name、age 和 5 的对象。注意,数值属性会自动转换为字符串。
console.log(person["name"]);
console.log(person.name);
从功能上讲,这两种存取属性的方式没有区别。使用括号的优势就是可以通过变量访问属性。
另外,如果属性名中包含可能会导致语法错误的字符,或者包含关键字/保留字时,也可以使用中括号语法,比如:
person["first name"] = "Nicholas";
因为"first name"中包含一个空格,所以不能用点语法来访问。不过,属性名中是可以包含非字母数字字符的,这时候只要使用中括号语法存取它们就行了。
6.2 Array
除了 Object,Array 应该就是 ECMAScript 中最常用的类型了。ECMAScript 数组跟其他编程语言的数组有很大区别。跟其他语言中的数组一样,ECMAScript 数组也是一组有序的数据,但跟其他余元不同的是,数组中每个槽位可以储存任意类型的数据。ECMAScript 数组也是动态大小的,会随着数据添加而自动增长。
6.2.1 创建数组
有几种基本的方式可以创建数组。一种是使用 Array 构造函数,比如:
let colors = new Array();
也可以给 Array 构造函数传入要保存的元素:
let colors = new Array("red", "blue", "green");
另一种创建数组的方式是使用数组字面量(array' literal)表示法。
6.2.4 检测数组
一个经典的ECMAScript问题是判断一个对象是不是数组。在只有一个网页(因而只有一个全局作用域)的情况下,使用 instanceof 操作符就足矣:
if (value instanceof Array){
// 操作数组
}
使用 instanceof 的问题是假定只有一个全局执行上下文。如果网页里有多个框架,则可能涉及两个不同的全局执行上下文,因此就会有两个不同版本的 Array 构造函数。如果要把数组从一个框架传给另一个框架,则这个数组的构造函数将有别于在第二个框架内本地创建的数组。
为解决这个问题,ECMAScript 提供了 Array.isArray ( ) 方法。这个方法的目的就是确定一个值是否为数组,而不用管它是在哪个全局执行上下文中创建的。来看下面的例子:
if (Array.isArray(value)){
// 操作数组
}
6.2.5 迭代器方法
在 ES6 中,Array 的原型上暴露了 3 个用于检索数组内容的方法:keys ( ) 、values ( ) 和 entries ( )。keys ( ) 返回数组索引的迭代器,values ( ) 返回数组元素的,entries ( ) 返回索引/值对的迭代器:
const a = ["foo", "bar", "baz", "qux"];
// 因为这些方法都返回迭代器,所以可以将它们的内容
// 通过Array.from()直接转换为数组实例
const aKeys = Array.from(a.keys());
const aValues = Array.from(a.values());
const aEntries = Array.from(a.entries());
console.log(aKeys); // [0,1,2,3]
console.log(aValues); // ["foo", "bar", "baz", "qux"]
console.log(aEntries); // [[0, "foo"], [1, "bar"], [2,"baz"], [3,"qux"]]
使用 ES6 的解构可以非常容易地在循环中拆分键/值对:
const a = ["foo", "bar", "baz", "qux"];
for (const [idx, element] of a.entries()){
alter(idx);
alter(element);
}
// 0
// foo
// 1
// bar
// 2
// baz
// 3
// qux
6.2.8 栈方法
ECMAScript 给数组提供几个方法,让它看起来像是另外一种数据结构。数组对象可以像栈一样,也就是一种限制插入和删除项的数据结构。栈是一种后进先出的结构,也就是最近添加的项先被删除。数据项的插入和删除只在栈的一个地方发生,即栈顶。ECMAScript 数组提供了 push ( ) 和 pop ( ) 方法,以实现类似栈的行为。
push ( ) 方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度。pop ( ) 方法则用于删除数组的最后一项,同时减少数组的 length 值,返回被删除的项。来看下面的例子:
let colors = new Array(); // 创建一个数组
let count = color.push("red", "green"); // 推入两项
alter(count); // 2
let item = colors.pop("black"); // 取得最后一项
alter(item); // black
alter(color.length); //2
这里创建了一个当栈来使用的数组 ( 注意不需要任何额外的代码,push ( ) 和 pop ( ) 都是数组的默认方法 )。首先,使用 push ( ) 方法把两个字符串推入数组末尾,将结果保存在变量 count 中 ( 结果为 2 )。
然后,再推入另一个值,再把结果保存在 count 中。因为现在数组中有 3 个元素,所以 push ( ) 返回 3 。在调用 pop ( ) 时,会返回数组的最后一项,即字符串"black"。此时数组还有两个元素。
栈方法可以与数组的其他任何方法一起使用,如下例所示:
let colors = ["red", "blue"]
colors.push("brown"); // 再添加一项
colors[3] = "black"; // 添加最后一项
alter(colors.length); // 4
let item = colors.pop(); // 取得最后一项
alter(item); // black
6.2.9 队列方法
以先进先出原则 ( FIFO, Firist-In-First-Out )形式限制访问。 队列在列表末尾添加数据,但从列表开头获取数据。因为有了在数据末尾添加数据的 push ( ) 方法,所以要模拟队列就差一个从数组开头获取数据的方法了。这个数组方法叫 shift ( ) ,它会删除数组的第一项并返回它,然后数组长度减 1 。使用 shift ( ) 和 push ( ) ,可以把数组当成队列来用。
let colors = new Array(); // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
alter(count); // 2
count = colors.push("black"); // 再推入一项
alter(count); // 3
let item = colors.shift(); // 取得第一项
alter(item); // red
alter(color.length); // 2
ECMAScript 也为数组提供了 unshift ( ) 方法。顾名思义,unshift ( ) 就是执行跟 unshift ( ) 相反的操作:在数组开头添加任意多个值, 然后返回新的数组的长度。通过使用 unshift ( ) 和 pop ( ) ,可以在相反方向上模拟队列。
let colors = new Array(); // 创建一个数组
let count = colors.unshift("red", "green"); // 推入两项
alter(count); // 2
count = colors.unshift("black"); // 再推入一项
alter(count); // 3
let item = colors.pop(); // 取得最后一项
alter(item); // green
alter(color.length); // 2
6.2.10 排序方法
数组有两个方法可以用来对元素重新排序:reverse ( ) 和 sort ( )。顾名思义,reverse ( ) 就是将数组元素反向排列。比如:
let values = [1, 2, 3, 4, 5];
values.reverse();
alter(values); // 5,4,3,2,1
这里,数组 values 的初始状态为 [1, 2, 3, 4, 5]。通过调用 reverse ( ) 反向排序,得到了 [5, 4, 3, 2, 1]。这个方法很直观,但不够灵活,所以才有了 sort ( ) 方法。
默认情况下,sort ( ) 会按照升序重新排列数组元素,即最小的值在前面,最大的值在后面。为此,sort ( ) 会在每一项上调用 String ( ) 转型函数,然后比较字符串来决定顺序。即使数组的元素都是数值,也会先把数组转换为字符串再比较、排序。比如:
let values = [0, 1, 5, 10, 15];
values.sort();
alter(values); // 0,1,10,15,5
一开始数组中数值的顺序是正确的,但调用 siort ( ) 会按照这些数值的字符串形式重新排序。因此即使 5 小于 10,但字符串"10"在字符串"5"的前头,所以 10 还是会排到 5 前面。很明显,这在多数情况下都不是最合适的。为此,sort ( ) 方法可以接收一个比较函数,用于判断哪个值应该排在前面。
比较函数接收两个参数,如果第一个参数应该排在第二个参数前面,就返回负值;如果两个参数相等,就返回 0 ;如果第一个参数应该排在第二个参数后面,就返回正值。下面是使用简单比较函数 ( 升序 ) 的一个例子:
function compare(value1, value2){
if (value1 < value2) {
return -1;
} else if (value1 > value2){
return 1;
} else {
return 0;
}
}
这个函数可以简写为一个箭头函数 ( 降序 ) :
let values = [0, 1, 5, 10, 15];
values.sort((a, b) => a < b ? 1 : a > b ? -1 : 0 );
alter(values); // 15,10,5,1,0
如果数组元素是数值,或者是其 valueOf ( ) 方法返回数值的对象 ( 如 Date 对象 ),这个比较函数还可以写得更简单,因为这时可以直接用第二个值减去第一个值:
function compare(value1, value2){
return value2 - value1;
}
比较函数就是要返回小于 0、0 和大于 0 的数值,因此减法操作完全可以满足要求。
6.2.11 操作方法
对于数组中的元素,我们有很多操作方法。比如,concat ( ) 方法可以在现有数组全部元素基础上创建一个新数组。它首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组。如果传入一个或多个数组,则 concat ( ) 会把这些数组的每一项都添加到结果数组。如果参数不是数组,则直接把它们添加到结果数组的末尾。来看下面的例子:
let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green", "blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]
打平数组参数的行为可以重写,方法是在参数数组上指定一个特殊符号:Symbol.isConcatSpreadable。这个符号能够阻止 concat ( ) 打平参数数组。相反,把这个值设置为 true 可以强制打平类数组对象:
let colors = ["red", "green", "blue"];
let newColors = ["black", "brown"];
let moreNewColors = {
[Symbol.isConcatSreadable]:true,
length: 2,
0: "pink",
1: "cyan"
};
newColors[Symbol.isSpreadable] = false;
// 强制不打平数组
let colors2 = colors.concat("yellow", newColors);
// 强制打平数组
let colors3 = colors.concat(moreNewColors);
console.log(colors); // ["red", "green", "blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", ["black", "brown"]]
console.log(colors3); // ["red", "green", "blue", "pink", "cyan"]
接下来,方法 slice ( ) 用于创建一个包含原有数组中一个或多个元素的新数组。slice ( ) 方法可以接收一个或两个参数:返回元素的开始索引和结束索引。如果只有一个参数,则 slice ( ) 返回该索引到数组末尾的所有元素。如果有两个参数>,则 slice ( ) 返回从开始索引到结束索引对应的所有元素,其中不包含结束索引>对应的元素。记住,这个操作不影响原数组。来看下面的例子:
let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
alter(colors2); // green,blue,yellow,purple
alter(colors3); // green,blue,yellow
6.2.12 搜索和位置方法
ECMAScript 提供两类搜索数组的方法:按严格相等搜索和按断言函数搜索。
1.严格相等
ECMAScript 提供了 3 个严格相等的搜索方法:indexOf ( ) 、lastIndexOf ( ) 和 includes ( ) 。其中,前两个方法再所有版本中都可用,而第三个方法是 ECMAScript 7 新增的。这些方法都接收两个参数:要查找的元素和一个可选的起始搜索位置。indexOf ( ) 和 includes ( ) 方法从数组前头 ( 第一项 ) 开始向后搜索,而 lastIndexOf ( ) 从数组末尾 ( 最后一项 ) 开始向前搜索。
indexOf ( ) 和 lastIndexOf ( ) 都返回要查找的元素再数组中的位置,如果没找到则返回 -1。includes ( ) 返回布尔值,表示是否至少找到一个与指定元素匹配的项。在比较第一个参数跟数组每一项时,会使用全等 ( === ) 比较,也就是说两项必须严格相等。下面来看一些例子:
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
alter(numbers.indexOf(4)); // 3
alter(numbers.lastIndexOf(4)); // 5
alter(numbers.includes(4)); // true
alter(numbers.indexOf(4, 4)); // 5
alter(numbers.lastIndexOf(4, 4)); // 3
alter(numbers.includes(4, 7)); // false
let person = { name: "Nicholas"};
let people = [{ name: "Nicholas"}];
let morePeople = [person];
alter(people.indexOf(person)); // -1
alter(morePeople.indexOf(person)); // 0
alter(people.includes(person)); // false
alter(morePeople.includes(person)) // true
2.断言函数
ECMAScript 也允许按照定义的断言函数搜索数组,每个索引都会调用这个函数。断言函数的返回值决定了相应索引的元素是否被认为匹配。
断言函数接收三个参数:元素、索引和数组本身。其中元素是数组中当前元素的索引,而数组就是正在搜索的数组。断言函数返回真值,表示是否匹配。
find ( ) 和 findIndex ( ) 方法使用了断言函数。这两个方法都从数组的最小索引开始。find ( ) 返回第一个匹配的元素,findIndex ( ) 返回第一个匹配元素的索引。这两个方法也都接收第二个可选的参数,用于指定断言函数内部 this 的值。
const people =[
{
name: "Matt",
age: 27
},
{
name: "Nicholas",
age: 29
}
];
alter(people.find((element, index, array) => element.age < 28));
// {name: "Matt", age:27}
alter(people.findIndex((element, index, array) => element.age < 28));
// 0
找到匹配项后,这两个方法都不再继续搜索。
const evens = [2, 4, 6];
// 找到匹配项后,永远不会检查数组的最后一个元素
evens.find((element, index, array) => {
console.log(element);
console.log(index);
console.log(array);
return element === 4;
});
// 2
// 0
// [2, 4, 6]
// 4
// 1
// [2, 4, 6]
6.2.13 迭代方法
ECMAScript 为数组定义了 5 个迭代方法。每个方法接收两个参数:以每一项参数运行的函数,以及可选的作为函数运行上下文的作用域对象 ( 影响函数中 this 的值 )。传给每个方法的函数接收 3 个参数:数组元素、元素索引和数组本身。因具体方法而异,这个函数的执行结果可能会也可能不会影响方法的返回值。数组的 5 个迭代方法如下。
- every ( ):对数组每一项都运行传入的函数,如果每一项函数都返回 true,则这个方法返回 true。
- filter ( ):对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回。
- forEach ( ):对数组每一项都运行传入的函数,没有返回值。
- map ( ):对数组每一项都运行传入的函数,返回每次函数调用的结果构成的数组。
- some ( ):对数组每一项都运行传入的函数,如果有一项函数返回 true ,则这个方法返回 true 。
这些方法都不改变调用它们的数组。
在这些方法中,every ( ) 和 some ( ) 是最相似的,都是从数组中搜索符合某个条件的元素。对 every ( ) 来说,传入的函数必须对每一项都返回 true,它才会返回 true;否则,他就返回 false。而对 some ( ) 来说,只要有一项让传入的函数返回 true ,他就会返回 true。下面是一个例子:
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every((item, index, array) => item > 2);
alter(everyResult); // false
let someResult = numbers.some((item, index, array) => item > 2);
alter(someResult); // true
以上代码调用了 every ( ) 和 some ( ),传入的函数都是在给定项大于 2 时返回 true。every ( ) 返回 false 是因为并不是每一项都能达到要求。而 some ( ) 返回 true 是因为至少有一项满足条件。
下面再看一看 filter ( ) 方法。这个方法基于给定的函数来决定某一项是否应该包含在它返回的数组中。比如,要返回一个所有数值都大于 2 的数组,可以使用如下代码:
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
alter(filterResult); // 3,4,5,4,3
这里调用 filter ( ) 返回的数组包含 3、4、5、4、3,因为只有对这些项传入的函数才返回 true。这个方法非常适合从数组中筛选满足给定条件的元素。
接下来 map ( ) 方法也会返回一个数组。这个数组的每一项都是对原始数组中同样位置的元素运行传入函数而返回结果。例如,可以将一个数组中的每一项都乘以 2 ,并返回包含所有结果的数组,如下所示:
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map((item, index, array) => item * 2);
alter(mapResult); // 2,4,6,8,10,8,6,4,2
以上代码返回了一个数组,包含原始数组中每个值乘以 2 的结果。这个方法非常适合创建一个与原始数组元素一一对应的新数组。
最后再来看一看 forEach ( ) 方法。这个方法只会对每一项运行传入的函数,没有返回值。本质上,forEach ( ) 方法相当于使用 for 循环遍历数组。比如:
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
// 执行某些操作
})
数组的这些迭代方法通过执行不同操作方便了对数组的处理。
6.3 定型数组
6.3.1 历史*
6.3.2 ArrayBuffer
ArrayBuffer ( ) 是一个普通的 JavaScript 构造函数,用于在内存中分配特定数量的字节空间。
const buf = new ArrayBuffer(16); // 在内存中分配 16 字节
alter(buf.byteLength); // 16
ArrayBuffer一经创建就不能再调整大小。不过,可以使用 slice ( ) 复制其全部或部分分到一个新实例中:
const buf1 = new ArrayBuffer(16);
const buf2 = buf1.slice(4, 12);
alter(buf2.byteLength); // 8
6.3.3 DateView*
6.3.4 定型数组*
6.4 Map
作为 ECMAScript 6的新增特性,Map 是一种新的集合类型,为这门语言带来了真正的键/值存储机制。Map 的大多数特性都可以通过 Object 类型实现,但二者之间还是存在一些细微的差异。
6.4.1 基本API*
使用 new 关键字和 Map 函数可以创建一个空映射:
const m = new Map();
6.4.2 顺序与迭代*
6.4.3 选择 Object 还是 Map
对于多数 Web 开发任务来说,选择 Object 还是 Map 只是个人偏好问题,影响不大。不过,对于在乎内存和性能的开发者来说,对象和映射之间确实存在显著差别。
1.内存占用
2.插入性能
3.查找速度
4.删除性能
6.5 WeakMap*
ECMAScript 6 新增的 ”弱映射“ ( WeakMap ) 是一种新的集合类型,为这门语言带来了增强的键/值对存储机制。WeakMap 是 Map 的 ”兄弟“类型,其 API 也是 Map的子集。WeakMap 中的”weak“ ( 弱 ),描述的是 JavaScript 垃圾回收程序对待 ”弱映射“ 中键的方式。
6.6 Set*
ECMAScript 6 新增的 Set 是一种新集合类型,为这门语言带来集合数据。Set 在很多方面都像是加强的 Map,这是因为它们的大多数 API 和行为都是共有的。
6.6.1 基本 API
使用 new 关键字和 Set 构造函数可以创建一个空集合:
const m = new Set();
如果想在创建的同时初始化实例,则可以给 Set 构造函数传入一个可迭代对象,其中需要包含插入到新集合实例中的元素:
// 使用数组初始化集合
const s1 = new Set(["val1", "val2", "val3"]);
alter(s1.size); // 3
// 使用自定义迭代器初始化集合
const s2 = new Set({
[Symbol.iterator]:function*(){
yield "val1";
yield "val2";
yield "val3";
}
});
alter(s2.size); // 3
初始化之后可以用 add ( ) 增加值,使用 has ( ) 查询,通过 size 取得元素数量,以及时使用 delete ( ) 和 clear ( ) 删除元素:
const s = new Set();
alter(s.has("Matt")); // false
alter(s.size); // 0
s.add("Matt")
.add("Frisbie");
alter(s.has("Matt")); // true
alter(s.size); // 2
s.delete("Matt");
alter(s.has("Matt")); // false
alter(s.has("Frisbie")); // true
alter(s.size); // 1
s.clear(); // 销毁集合实例中的所有值
alter(s.has("Matt")); // false
alter(s.has("Frisbie")); // false
alter(s.size); // 0
add ( ) 返回集合的实例,所以可以将多个添加操作连缀起来,包括初始化:
const s = new Set().add("val1");
s.add("val2").add("val3");
alter(s.size); // 3
6.7 WeakSet
6.8 迭代与拓展操作
6.9 小结
JavaScript 中的对象是引用值,可以通过几种内置引用类型创建特定类型的对象。
- 引用类型与传统面向对象编程语言中的类相似,但实现不同。
- Object 类型是一个基础类型,所有引用类型都从它继承了基本的行为。
- Array 类型表示一组有序的值,并提供了操作和转换值的能力。
- 定型数组包含一套不同的引用类型,用于管理数值在内存中的类型。
- Date 类型提供了关于日期和时间的信息,包括当前日期和时间的计算。
- RegExp 类型是ECMASript 支持的正则表达式的接口,提供了大多数基本正则表达式以及一些高级正则表达式的能力。
JavaScript 比较独特的一点是,函数其实是 Function 类型的实例,这意味着函数也是对象。由于函数是对象,因此也就具有能够增强自身行为的方法。
因为原始值包装类型的存在,所以 JavaScript 中原始值可以拥有类似对象的行为。有 3 种原始值包装类型:Boolean、Number 和 String。它们都具有如下特点。
- 每种包装类型都映射到同名的原始对象类型。
- 在以读模式访问原始值时,后台会实例化一个原始值包装对象,通过这个对象可以操作数据。
- 设计原始值的语句只要一执行完毕,包装对象就会立即销毁。
第七章 迭代器与生成器
本章内容
- 理解迭代
- 迭代模式
- 生成器
迭代的英文 ”iteration“ 源于拉丁文 itero,意思是 ”重复“ 或 ”再来“。在软件开发领域,”迭代” 的意思是按照顺序反复多次执行一段程序,通常会有明确的终止条件。ECMAScript 6 规范新增了两个高级特性:迭代器和生成器。使用这两个特性,能够更清晰、高效、方便地实现迭代。
7.1 理解迭代
在 JavaScript 中,计数循环就是一种最简单的迭代:
for(let i = 0; i < 10; ++i){
console.log(i);
}
循环是迭代的基础,这是因为它可以指定迭代的次数,以及每次迭代要执行什么操作。每次循环都会在下一次迭代开始之前完成,而每次迭代的顺序都是事先定义好的。
迭代会在一个有序集合上进行。( "有序" 可以理解为集合中所有项都可以按照既定的顺序被遍历到,特别是开始和结束项有明确的定义。 ) 数组是 JavaScript 中有序集合的最典型例子。
let collection = ['foo', 'bar', 'baz'];
for (let index = 0; index < collection.length; ++index){
console.log(collection[index]);
}
由于如下原因,通过这种循环来执行例程并不理想。
- 迭代之前需要事先知道如何使用数据结构。
- 遍历顺序并不是数据结构固有的。
ES 5 新增了 Array。prototype。forEach ( ) 方法,向通用迭代需求迈进了一步 ( 但仍然不够理想 ):
let collection = ['foo', 'bar', 'baz'];
collection.forEach((item) => console.log(item));
// foo
// bar
// baz
这个方法解决了单独记录索引和通过数组对象取得值的问题。不过,没有办法标识迭代何时终止。因此这个方法只适用于数组,而且回调结构也比较笨拙。
在 ECMAScript 较早的版本中,执行迭代必须使用循环或其他辅助结构。随着代码量增加,代码会变得越发混乱。很多语言都通过原生语言结果解决了这个问题,开发者无须事先知道如何迭代就能实现迭代操作。这个解决方案就是迭代器模式。Python、Java、C++,还有其他很多语言都对这个模式提供了完备的支持。JavaScript 在 ECMAScript 6 以后也支持了迭代器模式。
7.2 迭代器模式
7.2.1 可迭代协议
实现 Iterable 接口 ( 可迭代协议 ) 要求同时具备两种能力:支持迭代的自我识别能力和创建实现 Iterator 接口的对象的能力。在 ECMAScript 中,这意味着必须暴露一个属性作为 ”默认迭代器“ ,而且这个属性必须使用特殊的 Symbol。iterator 作为键。这个默认迭代器属性必须引用一个迭代器工厂函数,调用这个工厂函数必须返回一个新迭代器。
很多内置类型都实现了 Iterable 接口:
- 字符串
- 数组
- 映射
- 集合
- arguments 对象
- NodeList 等 DOM 集合类型
第八章 对象、类与面对对象编程
8.1 理解对象
8.1.1 属性的类型
8.1.2 定义多个属性
8.1.3 读取属性的特性
8.1.4 合并对象
8.1.7 对象解构
8.2 创建对象
8.2.1 概述
8.2.2 工厂模式
8.2.3 构造函数模式
8.2.4 原型模式
8.3 继承
8.4 类
8.4.1 类定义
8.5 小结
第九章 代理与反射
第十章 函数
10.1 箭头函数
ECMAScript 6 新增了使用胖箭头 ( => ) 语法定义函数表达式的能力。很大程度上,箭头函数实例化的对象与正式的函数的表达式创建的函数对象行为是相同的 。任何可以使用函数表达式的地方,都可以使用箭头函数:
let arrowSum = (a, b) => {
return a + b;
};
let functionExpressionSum = function(a, b) {
return a + b;
}
console.log(arrowSum(a, b)); // 13
console.log(functionExpressionSum(5, 8)); // 13
箭头函数简介的语法非常适合嵌入函数的场景:
let ints = [1, 2, 3];
console.log(ints.map(function(i) { return i + 1;})); // [2,3,4]
console.log(ints.map((i) => {return i + 1;}) // [2, 3, 4]
如果只有一个参数,那也可以不要括号。只要没有参数,或者多个参数的情况下,才需要使用括号:
// 以下两种写法都有效
let double = (x) => { return 2 * x; };
let triple = x => { return 3 * x; };
// 没有参数需要括号
let getRandom = () => { return Math.random(); };
// 多个参数需要括号
let sum = (a, b) => { return a + b; };
// 无效的写法:
let multiply = a, b => { return a * b; };
箭头函数也 可以不用大括号,但这样会改变函数的行为。使用大括号就说明包含 “函数体”,可以在一个函数中包含多条语句,跟常规的函数一样。如果不使用大括号,那么箭头函数后面只能有一行代码,比如一个赋值操作,或者一个表达式。而且,省略大括号会隐式返回这行代码的值:
// 以下两种写法都有效,而且返回相应的值
let double = (x) => { return 2 * x; };
let triple = (x) => 3 * x;
// 可以赋值
let value = {};
let setName = (x) => x.name = "Matt";
steName(vaule);
console,log(vaule.name); // "Matt"
// 无效的写法:
let multiply = (a, b) => return a * b;
箭头函数虽然语法简洁,但也有很多场合不适用。箭头函数不能使用 arguments、super 和 new.target,也不能用作构造函数。此外,箭头函数也没有 portotpye 属性。
10.2 函数名
因为函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量有相同的行为。这意味着一个函数可以有多个名称,如下所示:
function sun(num1, num2) {
return num1 + num2;
}
console.log(sum(10, 10)); // 20
let anotherSum = sum;
console.log(anotherSum(10, 10)) // 20
sum = null;
console.log(anotherSum(10, 10)); // 20
以上代码定义了一个名为sum ( ) 的函数,用于求两个数之和。然后有声明了一个变量 anotherSum,并将它的值设置等于sum。注意,使用不带括号的函数名会访问指针,而不会执行函数。此时,anotherSum 和 Sum 都指向同一个函数。调用 anotherSum ( ) 也可以返回结果。把 sum 设置为 null 之后,就切断了它与函数之间的关联。而 anotherSum ( ) 还是可以照常调用,没有问题。
ECMAScript 6 的所有函数对象都会暴露一个只读的 name 属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称。也会如实显示成空字符串。如果它是使用 Function 构造函数创建的,则会标识成"anonymous":
function foo() {};
let bar = function () {};
let baz = () => {};
console.log(foo.name); // foo
console.log(bar.name); // bar
console.log(baz.name); // baz
console.log((() => {}).name); // (空字符串)
console.log((new Function()).name); // anonymous
如果函数是一个获取函数、设置函数,或者使用bind ( ) 实例化,那么标识符前面会加上一个前缀:
function foo() {}
console.log(foo.bind(null)name); //bound foo
let dog = {
years: 1,
get age() {
return this.years;
},
set age(newAge) {
this.years = newAge;
}
}
let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age');
console.log(propertyDescriptor.get.name); // get age
console.log(propertyDescriptor.set.name); // set age
10.3 理解参数
10.4 没有重载
10.5 默认参数值
10.9函数内部
在 ECMAScript 5 中,函数内部存在两个特殊的对象:arguments 和 this。Script 6 又新增了 new.target 属性。
10.9.1 arguments
arguments 对象前面讨论很多次了,它是一个类数组对象,包含调用函数时传入的所有参数。这个对象只有以 function 关键字定义函数(相对于使用箭头函数语法创建函数)时才会有。虽然主要用于包含函数参数,但 arguments 对象其实还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针。来看下面这个经典的阶乘函数:
function factorial (num) {
if (num <= 1) {
return 1;
}else{
return num * factorial(num - 1);
}
}
阶乘函数一般定义成递归调用的,就像上面这个例子一样。只要给函数一个名称,而且这个名称不会变,这样定义就没有问题。但是,这个函数要正确的执行就必须保证函数名是 factorial,从而导致了紧密耦合。使用 arguments.callee 就可以让函数逻辑于函数名解耦:
function factorial (num) {
if (num <= 1) {
return 1;
}else{
return num * arguments.callee(num - 1);
}
这个重写之后的 factorial ( ) 函数已经用 arguments.callee 代替了之前硬编码的 factorial。这意味着无论函数叫什么名称,都可以引用正确的函数。考虑下面的情况:
let trueFactorial = factorial;
factorial = function () {
return 0;
};
console.log(trueFactorial(5)); // 120
console.log(factorial(5)); // 0
这里,trueFactorial 变量被赋值为 factorial,实际上把同一个函数的指针又保存到了另一个位置。然后,factorial 函数又被重写为一个返回 0 的函数。如果像 factorial ( ) 最初的版本那样不使用 arguments.callee,那么像上面这样调用 trueFactoial ( ) 就会返回 0。不过,通过将函数与名称解耦,trueFactorial ( ) 就可以正确计算阶乘,而 factorial ( ) 则只能返回 0。
10.9.2 this
另一个特殊的对象是 this,它在标准函数和箭头函数中有不同的行为。
在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在网页的全局上下文中调用函数时,this 指向 windows )。来看下面的例子:
windows.color = 'red';
let o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'blue'
定义在全局上下文中的函数 sayColor ( ) 引用了 this 对象。这个 this 到底引用哪个对象必须到函数调用时才能确定。因此这个值在代码执行的过程中可能会变。如果在全局上下文中调用 sayColor ( ),这结果会输出"red",因为 this 指向 window,而 this.color 相当于window.color。而在把 sayColor ( )赋值给 o 之后再调用 o.sayColor ( ),this会指向 o,即 this.color 相当于 o.color,所以会显示"blue"。
在箭头函数中,this 引用的时调用箭头函数的上下文。下面的例子演示了这一点。在对 sayColor ( ) 的两次调用中,this 引用的都是 window 对象,因为这个箭头函数是在 window 上下文中定义的:
windows.color = 'red';
let o = {
color: 'blue'
};
let sayColor = () => console.log(this.color);
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'red'
有读者知道,事件回调或定时回调中调用某个函数时,this 值指向的并非想要的对象。此使将回调函数写成箭头函数就可以解决问题。这是因为箭头函数中的 this 会保留定义该函数时的上下文:
function king() {
this.royaltyName = 'Henry';
// this 引用 King 的实例
setTimeout( () => console.log(this.rotylName), 1000);
}
function Queen() {
this.royaltyName = 'Elizabeth';
// this 引用 windows 对象
setTimeout(function() { console.log(this.royaltyName); }, 1000);
}
new King(); //Henry
new Queen(); // undefined
注意: 函数名只是保存指针的变量。因此全局定义的 sayColor ( ) 函数和 o.sayColor ( ) 时同一个函数,只不过执行的上下文不同。
10.9.3 caller
ECMAScript 5 也会给函数对象添加一个属性:caller。虽然 ECMAScript 3 中并没有定义,所有浏览器除了早期版本 Opera 都支持这个属性。这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为 null。比如:
function outer() {
inner();
}
function inner() {
console.log(inner.caller);
}
outer();
以上代码会显示 outer ( ) 函数的源代码。这是因为 outer ( ) 调用了 inner ( ),inner.caller 指向 outer ( )。如果要降低耦合度,则可以通过 arguments.callee.caller 来引用同样的值:
function outer() {
inner();
}
function inner() {
console.log(arguments.callee.caller);
}
在严格模式下访问 arguments.callee 会报错。ECMAScript 5 也定义了 arguments.caller,但在严格模式下访问它会报错,在严格模式下则始终是 undefined。这是为了分清 arguments.caller 和函数 caller 而故意为之的。而作为对这门语言的安全防护,这些改动也让第三方代码无法检测同意上下文中运行的其他代码。
严格模式下好友一个限制,就是不能给函数 caller 属性赋值,否则会导致错误。
10.9.4 new.target
ECMAScript 中函数始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。ECMAScript 6 新增了检测函数是否使用 new 关键字调用的 new.target 属性。如果函数是正常调用的,则 new.target 的值是 undefined; 如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数。
function King() {
if (!new,target) {
throw 'King must be instantiated using "new"'
}
console.log('King instantiated using "new"')
}
new King(); // King instantiated using "new
King(); // Error:King must be instantiated using "new"
10.10 函数属性与方法
前面提到过,ECAMScript 中的函数是对象,因此有属性和方法。每个函数都有两个属性:length 和 prototype。其中,length 属性保存函数定义的命名参数的个数,如下例所示:
function sayName(name) {
console.log(name);
}
function sum(num1, num2) {
return num1 + num2;
}
function sayHi() {
console.log("hi");
}
console.log(sayName.length); // 1
console.log(sum.length); // 2
console.log(sayHi.length); // 0
以上函数定义了 3 个函数,每个函数的命名参数个数都不一样。sayName ( ) 函数有一个命名参数,所以其 length 属性值为 1。类似地,sum ( ) 函数有两个命名参数,所以其 length 属性的值是 2。而 sayHi ( ) 没有命名参数,其 length 属性为 0。
prototype 属性也许是 ECMAScript 核心中最有趣的部分。prototype 是保存引用类型所有实例方法的地方,这意味着 toString ( ) 、valueOf ( ) 等方法实际上都保存在 prototype 上,进而由所有实例共享。这个属性在自定义类型时特别重要。在 ECMAScript 5 中,prototype 属性是不可枚举的,因此使用 for-in 循环不会返回这个属性。
函数还有两个方法:apply ( ) 和 call ( ) 。着两个方法都会以指定的 this 值来调用函数,即会设置调用函数时函数体内 this 对象的值。apply ( ) 方法接收两个参数:函数内 this 的值和一个参数数组。第二个参数可以是 Array 的实例,但也可以是 arguments 对象。来看下面的例子:
function sum(num1, num2) {
return num1 + num2;
}
function callSum1(num1, num2) {
return sum.apply(this, arguments); // 传入 arguments 对象
}
function callSum2(num1, num2) {
return sum.apply(this, [num1, num2]); // 传入数组
}
console.log(callSum1(10, 10)); // 20
console.log(callSum2(10, 10)); // 20
在这个例子中,callSum1 ( ) 会调用 sum ( ) 函数,将 this 作为函数体内 this 值(这里等于 window,因为是在全局作用域中调用的)传入,同时还传入了 agruments 对象。callNum2 ( ) 也会调用 sum ( ) 函数,但会传入参数的数组。这两个函数都会执行并返回正确的结果。
第十一章 期约与异步函数
11.1 异步编程
异步行为是为了优化因计算量大而时间长的操作。如果在等待其他操作完成的同时,即使运行其他指令,系统也能保持稳定,那么这样做就是务实的。
11.1.1 同步与异步
同步行为对应内存中顺序执行的处理器指令。
在程序执行的每一步,都可以推断出程序的状态。这是因为后面的指令总是在前面的指令完成后才会执行。
相对的,异步行为类似于系统中断,即当前进程外部的实体可以触发代码执行。异步操作经常是必要的,因为强制进程等待一个长时间的操作通常是不可行的。如果代码要访问一些高延迟的资源,比如向远程服务器发送请求并等待响应,那么就会出现长时间等待。
11.1.2 以往的异步编程模式
异步行为是 JavaScript 的基础,但以前的实现不理想。在早期的 JavaScript 中,只支持定义回调函数来表明异步操作完成。串联多个异步操作是一个常见的问题,通常需要深度嵌套的回调函数(俗称 “回调地狱”)来解决。
假设有以下异步函数,使用了 setTimeout 在一秒钟之后执行某些操作:
function double(value) {
setTimeout(() => setTimeout(console.log, 0, value * 2), 1000)
}
double(3);
// 6 (大约1000毫秒之后)
这里的代码没什么神秘的,但关键是理解为什么说它是一个异步函数。setTimeout 可以定义一个在指定时间之后会被调度执行的回调函数。对这个例子而言,1000毫秒之后,JavaScript 运行时会把回调函数推到自己的消息队列上去等待执行。推到队列之后,回调什么时候出列被执行对 JavaScript 代码就完全不可见了。还有一点,double ( ) 函数在 setTimeout 成功调度异步操作之后会立即退出。
1.异步返回值
假设 setTimeout 操作会返回一个有用的值。有什么好办法把这个值传给需要它的地方?广泛接受的一个策略是给异步操作一个回调,这个回调中包含要使用异步返回值的代码(作为回调的参数)。
function double(value, callback) {
setTimeout(() => callback(value * 2), 1000)
}
double(3, (x) => console.log('I was given: ${x}'));
// I was given: 6 (大约1000毫秒之后)
2.失败处理
异步操作的失败处理在回调模型中也要考虑,因此自然就出现了成功回调和失败回调:
function double(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== 'number') {
throw 'Must provide number as first argument';
}
success(2 * value);
} catch (e) {
failure(e);
}
}, 1000);
}
const successCallback = (x) => console.log('Success: ${x}');
const failureCallback = (e) => console.log('Success: ${e}');
double(3, successCallback, failureCallback);
double('b', successCallback, failureCallback);
// Success: 6(大约 1000 毫秒之后)
// Failure: Must provide number as first argument (大约 1000 毫秒之后)
这种模式已经不可取了,因为必须在初始化异步操作时定义回调。异步函数的返回值只在短时间内存在,只有预备好将这个短时间内存在的值作为参数的回调才能接收到它。
3.嵌套异步回调
如果异步返回值又依赖另一个异步返回值,那么回调的情况还会进一步变复杂。在实际的代码中,这就要求嵌套:
function double(value, success, failure) {
setTimeout(() => {
try {
if (typeof value !== 'number') {
throw 'Must provide number as first argument';
}
success(2 * value);
catch (e) {
failure(e);
}
}
}, 1000);
}
const successCallback = (x) => {
double(x, (y) => console.log('Success: ${y}'));
};
const failureCallback = (e) => console.log('Success: ${e}');
double(3, successCallback, failureCallback);
11.2 期约
期约是对尚不存在结果的一个替身。期约(promise)这个名字最早是由 Daniel Friedman 和 David Wise 在他们与 1976 年发表的论文 “The Impact of Applicative Programming on Multiprocessing” 中提出来的。