Day18-D22
B1.何为ES6
1.介绍
1)ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在
2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复
杂的大型应用程序,成为企业级开发语言。
2.ECMAScript与JavaScript 的关系
1)前者是后者的规格,后者是前者的一种实现
(另外的 ECMAScript 方言还有 JScript 和 ActionScript)
3.ES6简介
1)ES6 = ECMAScript 这门标准的第 6 代版本(2015)。
- ECMAScript 是语言的标准
- 6 是版本号
- ECMA:欧洲计算机制造商协会
具体内容:语法 + API
历史版本:ES1——>3、ES5——>6(ES4 被废弃)
我们目前使用 JS 的大部分内容都是 ES3 的部分。
ES 与 JS 的关系:JavaScript(浏览器端) = ESMAScript(语法+API) + DOM + BOM
ES6 的兼容性:
- 主流浏览器的最新版本几乎都全部支持 ES6
- IE 老版本等不支持的浏览器,可以使用 Babel 转码
- 总之,请放心大胆地使用 ES6
B2.let 和 const
1.let命令
A.基本用法
1)ES6 新增了命令,用来声明变量。它的用法类似如下,但所声明的变量,只在命令所在的代码块内有效{ let a = 10; var b = 1; } a // ReferenceError: a is not defined. b // 12)上面代码在代码块之中,分别用和声明了两个变量。然后在代码块之外调用这两个变量,结果声明的变量报错,声明的变量返回了正确的值。这表明,声明的变量只在它所在的代码块有效
for循环的计数器,就很合适使用命令for (let i = 0; i < 10; i++) { // ... } console.log(i); // ReferenceError: i is not defined3)上面代码中,计数器只在循环体内有效,在循环体外引用就会报错
下面的代码如果使用,最后输出的是var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 104)上面代码中,变量是命令声明的,在全局范围内都有效,所以全局只有一个变量。每一次循环,变量的值都会发生改变,而循环内被赋给数组的函数内部的,里面的指向的就是全局的。也就是说,所有数组的成员里面的,指向的都是同一个,导致运行时输出的是最后一轮的的值,也就是 10
如果使用,声明的变量仅在块级作用域内有效,最后输出的是 6var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 65)上面代码中,变量是声明的,当前的只在本轮循环有效,所以每一次循环的其实都是一个新的变量,所以最后输出的是。你可能会问,如果每一轮循环的变量都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量时,就在上一轮循环的基础上进行计算
另外,循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); } // abc // abc // abc6)上面代码正确运行,输出了 3 次。这表明函数内部的变量与循环变量不在同一个作用域,有各自单独的作用域(同一个作用域不可使用 重复声明同一个变量)
B.不存在变量声明
1)var命令会发生"变量提升"现象,即变量可以在声明之前使用,值为。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用
为了纠正这种现象,命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错// var 的情况 console.log(foo); // 输出undefined var foo = 2; // let 的情况 console.log(bar); // 报错ReferenceError let bar = 2;2)上面代码中,变量用命令声明,会发生变量提升,即脚本开始运行时,变量已经存在了,但是没有值,所以会输出。变量用命令声明,不会发生变量提升。这表示在声明它之前,变量是不存在的,这时如果用到它,就会抛出一个错误
C.暂时性死区
1)只要作用域内存在 let、const,它们所声明的变量或常量就自动 “绑定” 这个区域,不再受到外部作用域的影响let a = 2; function func() { console.log(a); // 报错 let a = 1; } func();let a = 2; function func() { console.log(a); // 2 } func();2)即:只要作用域内出现了同名的 let 或 const,那么就会去找这个量(向前找),如果找不到也不会跳去外部找,只会直接报错!
只要我们遵守 “先声明后使用”,那么其实就基本不会遇到变量提升及暂时性死区问题
D.不允许重复声明
1)let不允许在相同作用域内,重复声明同一个变量// 报错 function func() { let a = 10; var a = 1; } // 报错 function func() { let a = 10; let a = 1; }2)因此,不能在函数内部重新声明参数
function func(arg) { let arg; } func() // 报错 function func(arg) { { let arg; } } func() // 不报错2.块级作用域
A.为什么需要块级作用域
1)ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景
2)第一种场景,内层变量可能会覆盖外层变量var tmp = new Date(); function f() { console.log(tmp); if (false) { var tmp = 'hello world'; } } f(); // undefined3)上面代码的原意是,代码块的外部使用外层的变量,内部使用内层的变量。但是,函数执行后,输出结果为,原因在于变量提升,导致内层的变量覆盖了外层的变量
第二种场景,用来计数的循环变量泄露为全局变量var s = 'hello'; for (var i = 0; i < s.length; i++) { console.log(s[i]); } console.log(i); // 5上面代码中,变量只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量
B.ES6的块级作用域
1)let实际上为 JavaScript 新增了块级作用域- 作用域链:内层作用域 ——> 外层作用域 ——> 全局作用域
- 块级作用域:除了对象 ,函数 (函数作用域)之外的一切 都属于块级作用域
function f1() { let n = 5; if (true) { let n = 10; } console.log(n); // 5 }上面的函数有两个代码块,都声明了变量,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用定义变量,最后输出的值才是 10
2)ES6 允许块级作用域的任意嵌套{{{{ {let insane = 'Hello World'} console.log(insane); // 报错 }}}};上面代码使用了一个五层的块级作用域,每一层都是一个单独的作用域。第四层作用域无法读取第五层作用域的内部变量
3)内层作用域可以定义外层作用域的同名变量{{{{ let insane = 'Hello World'; {let insane = 'Hello World'} }}}};4)块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了
// IIFE 写法 (function () { var tmp = ...; ... }()); // 块级作用域写法 { let tmp = ...; ... }C.块级作用域与声明函数
1)ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明// 情况一 if (true) { function f() {} } // 情况二 try { function f() {} } catch(e) { // ... }上面两种函数声明,根据 ES5 的规定都是非法的
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错
2)ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于,在块级作用域之外不可引用function f() { console.log('I am outside!'); } (function () { if (false) { // 重复声明一次函数f function f() { console.log('I am inside!'); } } f(); }());上面代码在 ES5 中运行,会得到“I are inside!”,因为在内声明的函数会被提升到函数头部,实际运行的代码如下
// ES5 环境 function f() { console.log('I am outside!'); } (function () { function f() { console.log('I am inside!'); } if (false) { } f(); }());3)ES6 就完全不一样了,理论上会得到“我在外面!”。因为块级作用域内声明的函数类似于,对作用域之外没有影响。但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的
// 浏览器的 ES6 环境 function f() { console.log('I am outside!'); } (function () { if (false) { // 重复声明一次函数f function f() { console.log('I am inside!'); } } f(); }()); // Uncaught TypeError: f is not a function上面的代码在 ES6 浏览器中,都会报错
4)原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响- 允许在块级作用域内声明函数
- 函数声明类似于,即会提升到全局作用域或函数作用域的头部
- 同时,函数声明还会提升到所在的块级作用域的头部
注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作处理
5)根据这三条规则,浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于声明的变量。上面的例子实际运行的代码如下// 浏览器的 ES6 环境 function f() { console.log('I am outside!'); } (function () { var f = undefined; if (false) { function f() { console.log('I am inside!'); } } f(); }()); // Uncaught TypeError: f is not a function6)考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句
// 块级作用域内部的函数声明语句,建议不要使用 { let a = 'secret'; function f() { return a; } } // 块级作用域内部,优先使用函数表达式 { let a = 'secret'; let f = function () { return a; }; }7)另外,还有一个需要注意的地方。ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域
// 第一种写法,报错 if (true) let x = 1; // 第二种写法,不报错 if (true) { let x = 1; }上面代码中,第一种写法没有大括号,所以不存在块级作用域,而只能出现在当前作用域的顶层,所以报错。第二种写法有大括号,所以块级作用域成立
8)函数声明也是如此,严格模式下,函数只能声明在当前作用域的顶层// 不报错 'use strict'; if (true) { function f() {} } // 报错 'use strict'; if (true) function f() {}3.const 命令
A.基本用法
1)const声明一个只读的常量。一旦声明,常量的值就不能改变const PI = 3.1415; PI // 3.1415 PI = 3; // TypeError: Assignment to constant variable.上面代码表明改变常量的值会报错
2)const声明的变量不得改变值,这意味着,一旦声明变量,就必须立即初始化,不能留到以后赋值const foo; // SyntaxError: Missing initializer in const declaration上面代码表示,对于来说,只声明不赋值,就会报错
3)const的作用域与命令相同:只在声明所在的块级作用域内有效if (true) { const MAX = 5; } MAX // Uncaught ReferenceError: MAX is not defined4)const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用
if (true) { console.log(MAX); // ReferenceError const MAX = 5; }上面代码在常量声明之前就调用,结果报错
5)const声明的常量,也与一样不可重复声明var message = "Hello!"; let age = 25; // 以下两行都会报错 const message = "Goodbye!"; const age = 30;B.本质
1)const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心const foo = {}; // 为 foo 添加一个属性,可以成功 foo.prop = 123; foo.prop // 123 // 将 foo 指向另一个对象,就会报错 foo = {}; // TypeError: "foo" is read-only上面代码中,常量储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性
2)下面是另一个例子const a = []; a.push('Hello'); // 可执行 a.length = 0; // 可执行 a = ['Dave']; // 报错上面代码中,常量是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给,就会报错
C.window 对象的属性和方法(全局作用域中)
1)全局作用域中, 声明的变量, 声明的函数,会自动变成 window 对象的属性或方法var age = 18; function add() {} console.log(window.age); // 18 console.log(window.add === add); // truelet age = 18; const add = function() {} console.log(window.age); // undefined console.log(window.add === add); // falseD.什么时候用 let,什么使用用 const
1)原则:如果不知道用什么的时候,就用 const
2)原因:如果应该是常量,那么刚好符合需求。如果应该是变量,那么后来报错时,再来改为变量也为时不晚。同时,一开始就设置为常量还会避免真的需要为常量时,该值在后来被意外修改的情况
4.let和const总结
1)let 声明的变量会产生块作用域,var 不会产生块作用域
2)const 声明的常量也会产生块作用域
3)不同代码块之间的变量无法互相访问
4)注意: 对象属性修改和数组元素变化不会出发 const 错误 (数组和对象存的是引用地址)
5)应用场景:声明对象类型使用 const,非对象类型声明选择 let
6)cosnt声明必须赋初始值,标识符一般为大写,值不允许修改
5.顶层对象的属性
1)顶层对象,在浏览器环境指的是对象,在 Node 指的是对象。ES5 之中,顶层对象的属性与全局变量是等价的window.a = 1; a // 1 a = 2; window.a // 2上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事
2)顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的
3)ES6 为了改变这一点,一方面规定,为了保持兼容性,命令和命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,命令、命令、命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩var a = 1; // 如果在 Node 的 REPL 环境,可以写成 global.a // 或者采用通用方法,写成 this.a window.a // 1 let b = 1; window.b // undefined上面代码中,全局变量由命令声明,所以它是顶层对象的属性;全局变量由命令声明,所以它不是顶层对象的属性,返回
B3.解构赋值
1.数组的解构赋值
A.原理
1)ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructure
以前,为变量赋值,只能直接指定值let a = 1; let b = 2; let c = 3;ES6 允许写成下面这样
let [a, b, c] = [1, 2, 3];上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值
a.模式(结构)匹配[] = [1, 2, 3];
b.索引值相同的完成赋值const [a, b, c] = [1, 2, 3];
c.举例const [a, [, , b], c] = [1, [2, 3, 4], 5]; console.log(a, b, c); // 1 4 5B.数组解构赋值的默认值
1)默认值的基本用法const [a, b] = []; console.log(a, b); // undefined undefined // --------------------------------------- const [a = 1, b = 2] = []; console.log(a, b); // 1 22)默认值的生效条件
只有当一个数组成员严格等于 (===) undefined 时,对应的默认值才会生效
const [a = 1, b = 2] = [3, 0]; // 3 0 const [a = 1, b = 2] = [3, null]; // 3 null const [a = 1, b = 2] = [3]; // 3 23)默认值表达式
如果默认值是表达式,默认值表达式是惰性求值的(即:当无需用到默认值时,表达式是不会求值的const func = () => { return 24; }; const [a = func()] = [1]; // 1 const [b = func()] = []; // 24C.数组解构赋值的应用
1)论据function func() { const [a, b] = arguments; console.log(a, b); // 1 2 } func(1, 2);2)节点列表
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>NodeList</title> </head> <body> <p>1</p> <p>2</p> <p>3</p> <script> const [p1, p2, p3] = document.querySelectorAll('p'); console.log(p1, p2, p3); /* <p>1</p> <p>2</p> <p>3</p> */ </script> </body> </html>3)函数参数的解构赋值
const array = [1, 1]; // const add = arr => arr[0] + arr[1]; const add = ([x = 0, y = 0]) => x + y; console.log(add(array)); // 2 console.log(add([])); // 04)交换变量的值
let x = 2, y = 1; // 原来 let tmp = x; x = y; y = tmp; // 现在 [x, y] = [y, x]; // 理解:[x, y] = [2, 1] console.log(x, y); // 1 25)跳过某项值使用逗号隔开
在解构数组时,可以忽略不需要解构的值,可以使用逗号对解构的数组进行忽略操作,这样就不需要声明更多的变量去存值了:var [a, , , b] = [10, 20, 30, 40]; console.log(a); // 10 console.log(b); // 40上面的例子中,在 a、b 中间用逗号隔开了两个值,这里怎么判断间隔几个值呢,可以看出逗号之间组成了多少间隔,就是间隔了多少个值。如果取值很少的情况下可以使用下标索引的方式来获取值
6)剩余参数中的使用
通常情况下,需要把剩余的数组项作为一个单独的数组,这个时候我们可以借助展开语法把剩下的数组中的值,作为一个单独的数组,如下:var [a, b, ...rest] = [10, 20, 30, 40, 50]; console.log(a); // 10 console.log(b); // 20 console.log(rest); // [30, 40, 50]在 rest 的后面不能有 逗号 不然会报错,程序会认出你后面还有值。是剩余参数的解构,所以只能放在数组的最后,在它之后不能再有变量,否则则会报错
D. 必须要分号的两种情况// 1. 立即执行函数 // ;(function () { })(); // (function () { })(); // 2. 使用数组解构的时候 // const arr = [1, 2, 3] const str = 'pink'; [1, 2, 3].map(function (item) { console.log(item) }) let a = 1 let b = 2 ;[b, a] = [a, b] console.log(a, b)2.对象的解构赋值
A.原理
1)对象的解构和数组基本类似,对象解构的变量是在 中定义的。对象没有索引,但对象有更明确的键,通过键可以很方便地去对象中取值。在 ES6 之前直接使用键取值已经很方便了:var obj = { name: 'imooc', age: 7 }; var name = obj.name; // imooc var age = obj.age; // 72)但是在 ES6 中通过解构的方式,更加简洁地对取值做了简化,不需要通过点操作增加额外的取值操作
var obj = { name: 'imooc', age: 7 }; var { name, age } = obj; // name: imooc, age: 7在 直接声明 name 和 age 用逗号隔开即可得到目标对象上的值,完成声明赋值操作
a.模式(结构)匹配{} = {};
b.属性名相同的完成赋值或const {name, age} = {name: 'jerry', age: 18};const {age, name} = {name: 'jerry', age: 18};
B.对象解构赋值的默认值
1)对象的属性值严格等于 undefined 时,对应的默认值才会生效
2)如果默认值是表达式,默认值表达式是惰性求值的
对象的默认值和数组的默认值一样,只能通过严格相等运算符(===)来进行判断,只有当一个对象的属性值严格等于 ,默认值才会生效var {a = 10, b = 5} = {a: 3}; // a = 3, b = 5 var {a = 10, b = 5} = {a: 3, b: undefined}; // a = 3, b = 5 var {a = 10, b = 5} = {a: 3, b: null}; // a = 3, b = null所以这里的第二项 b 的值是默认值,第三项的 的值为 false,所以 b 的值为 null
C.重命名属性
1)在对象解构出来的变量不是我们想要的变量命名,这时我们需要对它进行重命名var {a:x = 8, b:y = 3} = {a: 2}; console.log(x); // 2 console.log(y); // 3这里把 a 和 b 的变量名重新命名为 x 和 y
D.对象解构赋值的应用
1)对象作为函数参数// 之前 const logPersonInfo = user => console.log(user.name, user.age); logPersonInfo({name: 'jerry', age: 18}); // 之后 const logPersonInfo = ({age = 21, name = 'ZJR'}) => console.log(name, age); logPersonInfo({name: 'jerry', age: 18}); // jerry 18 logPersonInfo({}); // ZJR 212)复杂的嵌套(主要是缕清逻辑关系即可)
const obj = { x: 1, y: [2, 3, 4], z: { a: 5, b: 6 } }; // ---------------------------------------------------- const {x, y, z} = obj; console.log(x, y, z); // 1 [ 2, 3, 4 ] { a: 5, b: 6 } // ---------------------------------------------------- const {y: [, y2]} = obj; console.log(y2); // 3 console.log(y); // 报错 // ---------------------------------------------------- const {y: y, y: [, y2]} = obj; console.log(y2); // 3 console.log(y); // [ 2, 3, 4 ] // ---------------------------------------------------- const {y, y: [, y2], z, z: {b}} = obj; console.log(y2); // 3 console.log(y); // [ 2, 3, 4 ] console.log(z); // { a: 5, b: 6 } console.log(b); // 63)剩余参数中的使用
在对象的解构中也可以使用剩余参数,对对象中没有解构的剩余属性做聚合操作,生成一个新的对象var {a, c, ...rest} = {a: 1, b: 2, c: 3, d: 4} console.log(a); // 1 console.log(c); // 3 console.log(rest); // { b: 2, d: 4 }对象中的 b、d 没有被解构,通过剩余参数语法把没有解构的对象属性聚合到一起形成新的对象
E.注意点
1)如果要将一个已经声明的变量用于解构赋值,必须非常小心// 错误的写法 let x; {x} = {x: 1}; // SyntaxError: syntax error上面代码的写法会报错,因为 JavaScript 引擎会将理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题
2)解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式({} = [true, false]); ({} = 'abc'); ({} = []);上面的表达式虽然毫无意义,但是语法是合法的,可以执行
3)由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构let arr = [1, 2, 3]; let {0 : first, [arr.length - 1] : last} = arr; first // 1 last // 3上面代码对数组进行对象解构。数组的键对应的值是,就是键,对应的值是
3.字符串的解构赋值
1)既可以用数组的形式来解构赋值,也可以用对象的形式来解构赋值// 数组形式解构赋值 const [a, b, , , c] = 'hello'; console.log(a, b, c); // h e o // 对象形式解构赋值 const {0: a, 1: b, 4: o, length} = 'hello'; console.log(a, b, o, length); // h e o 54.数值和布尔值的解构赋值
1)只能按照对象的形式来解构赋值
(会先自动将等号右边的值转为对象)// 先来复习一下将数值和布尔值转化为对象 console.log(new Number(123)); console.log(new Boolean(true)); // 转化后的对象里没有任何的属性(没有 123 这个属性,也没有 true 这个属性)和方法, // 所有的属性和方法都在它的继承 __proto__ 中,比如 toString 方法就是继承来的。 // 里面的值只能是默认值,继承的方法倒是可以取到 const {a = 1, toString} = 123; console.log(a, toString); // 1 [Function: toString] // 里面的值只能是默认值,继承的方法倒是可以取到 const {b = 1, toString} = true; console.log(b, toString); // 1 [Function: toString]知道有这回事即可,一般都用不到,因为没太大意义
5.undefined 和 null 没有解构赋值
1)由于 undefined 和 null 无法转为对象,所以对它们进行解构赋值,都会报错
6.小结
1)解构赋值一般针对对象和数组,如果解构对象是 或是 都会报错
2)默认值的生效条件是,只有当解构的对象的值是严格模式下的 的情况下,默认值才会生效
3)可以不借助中间变量来交换两个值
4)在解构复杂的数据解构时,注意声明的对象要和目标的对象有着相同的解构形式,才能去解构目标对象B4.函数的扩展
1.函数参数的默认值
A.认识函数参数的默认值
1)调用函数的时候传参了,就用传递的参数;如果没传参,就用默认值
B.函数参数默认值的基本用法// 之前的默认值实现方式 const multiply = (x, y) => { if (typeof y === 'undefined') { y = 3; } return x * y; }; console.log(multiply(2, 2)); // 4 console.log(multiply(2)); // 6// ES6 默认值实现方式 const multiply = (x, y = 3) => { return x * y; }; console.log(multiply(2, 2)); // 4 console.log(multiply(2)); // 6C.默认值的生效条件
1)不传参数,或者明确的传递 undefined 作为参数,只有这两种情况下,默认值才会生效
2)注意:null 就是 null,不会使用默认值
D.与解构赋值默认值结合使用
1)参数默认值可以与解构赋值的默认值,结合起来使用function foo({x, y = 5}) { console.log(x, y); } foo({}) // undefined 5 foo({x: 1}) // 1 5 foo({x: 1, y: 2}) // 1 2 foo() // TypeError: Cannot read property 'x' of undefined上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数foo()的参数是一个对象时,变量x和y才会通过解构赋值生成。如果函数foo()调用时没提供参数,变量x和y就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况
2)function foo({x, y = 5} = {}) { console.log(x, y); } foo() // undefined 5上面代码指定,如果没有提供参数,函数foo的参数默认为一个空对象
3)下面是另一个解构赋值默认值的例子function fetch(url, { body = '', method = 'GET', headers = {} }) { console.log(method); } fetch('http://example.com', {}) // "GET" fetch('http://example.com') // 报错上面代码中,如果函数fetch()的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值
4)function fetch(url, { body = '', method = 'GET', headers = {} } = {}) { console.log(method); } fetch('http://example.com') // "GET"上面代码中,函数fetch没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method才会取到默认值
5)注意,函数参数的默认值生效以后,参数解构赋值依然会进行function f({ a, b = 'world' } = { a: 'hello' }) { console.log(b); } f() // world上面示例中,函数f()调用时没有参数,所以参数默认值{ a: ‘hello’ }生效,然后再对这个默认值进行解构赋值,从而触发参数变量b的默认值生效
E.参数默认值的位置
1)通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的// 例一 function f(x = 1, y) { return [x, y]; } f() // [1, undefined] f(2) // [2, undefined] f(, 1) // 报错 f(undefined, 1) // [1, 1] // 例二 function f(x, y = 5, z) { return [x, y, z]; } f() // [undefined, 5, undefined] f(1) // [1, 5, undefined] f(1, ,2) // 报错 f(1, undefined, 2) // [1, 5, 2]上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined
2)如果传入undefined,将触发该参数等于默认值,null则没有这个效果function foo(x = 5, y = 6) { console.log(x, y); } foo(undefined, null) // 5 null上面代码中,x参数对应undefined,结果触发了默认值,y参数等于null,就没有触发默认值
F.函数参数默认值的应用
1)接收很多参数的时候// 普通时候 const logUser = (username = 'zjr', age = 18, sex = 'male') => { console.log(username, age, sex); }; // 需要能够记住参数的顺序,如果参数较多那么需要配合文档,使用不方便 logUser('jerry', 18, 'male'); // ------------------------------------------------------------ // 接收一个对象作为参数 // 不需要记住参数的顺序 const logUser = options => { console.log(options.username, options.age, options.sex); }; logUser({ username: 'jerry', age: 18, sex: 'male' }); // ------------------------------------------------------------ // 再优化 const logUser = ({username, age, sex}) => { console.log(username, age, sex); }; logUser({ username: 'jerry', age: 18, sex: 'male' }); // ------------------------------------------------------------ // 引入默认值 const logUser = ({ username = 'zjr', age = 18, sex = 'male' }) => { console.log(username, age, sex); }; // 其实是解构赋值原理 logUser({username: 'jerry'}); // jerry 18 male logUser({}); // zjr 18 male logUser(); // 报错,因为这样相当于传了一个 undefined,不符合解构赋值 // ------------------------------------------------------------ // 再优化(函数默认值 + 解构赋值 + 解构赋值默认值) const logUser = ({ username = 'zjr', age = 18, sex = 'male' } = {}) => { console.log(username, age, sex); }; logUser(); // zjr 18 male /* 解释: 1、options 与 {username = 'zjr', age = 18, sex = 'male'} 互等 2、{username = 'zjr', age = 18, sex = 'male'} = {} 其实就是 options = {} 3、由于 logUser() 的实参为 undefined,所以默认值为 {} 4、再因为 {username = 'zjr', age = 18, sex = 'male'} = {} 是解构赋值 5、由于 {} 内为 undefined,所以解构赋值启用默认值 5、所以真正的形参为 {username = 'zjr', age = 18, sex = 'male'} 注明:这样做的好处是增加函数的健壮性! */2)某一个参数不得省略,如果省略就抛出一个错误
function throwIfMissing() { throw new Error('Missing parameter'); } function foo(mustBeProvided = throwIfMissing()) { return mustBeProvided; } foo() // Error: Missing parameter上面代码的foo函数,如果调用的时候没有参数,就会调用默认值throwIfMissing函数,从而抛出一个错误
从上面代码还可以看到,参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(注意函数名throwIfMissing之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行
3)另外,可以将参数默认值设为undefined,表明这个参数是可以省略的function foo(optional = undefined) { ··· }2.rest 参数
A.前言
1)剩余语法(Rest syntax 也可以叫剩余参数)看起来和展开语法完全相同都是使用 … 的语法糖,不同之处在于剩余参数用于解构数组和对象
从某种意义上说,剩余语法与展开语法是相反的:展开语法将数组展开为其中的各个元素,而剩余语法则是将多个元素收集起来成为一个整体
B.函数参数
1)在 ES5 中,函数经常会传入不定参数,在传入不定参数时,ES5 的给出的解决方案是通过 arguments 对象来获取函数调用时传递的参数
arguments 对象不是一个数组,它是一个类数组对象,所谓类数组对象,就是指可以通过索引属性访问元素并且拥有 length 属性的对象
一个简单的类数组对象是长这样的:var arrLike = { 0: 'name', 1: 'age', 2: 'job', length: 3 }2)而它所对应的数组应该是这样子的:
var arr = ['name', 'age', 'job'];这里我们说类数组对象与数组的性质相似,是因为类数组对象在访问、赋值、获取长度上的操作与数组是一致的,具体内容可查阅相关的类数组使用
3)在函数体中定义了 Arguments 对象,其包含函数的参数和其它属性,以 arguments 变量来指代。下面我们看个实例:function fn() { console.log(arguments); } fn('imooc', 7, 'ES6')4)在控制台中打印出上面的代码结果,如下图所示:在定义函数的时候没有给定参数,但是通过 arguments 对象可以拿到传入的参数。可以看到 arguments 中包含了函数传递的参数、length 等属性,length 属性表示的是实参的长度,即调用函数的时候传入的参数个数。这样我们就对 arguments 对象有了一定的了解

5)在 ES5 的开发模式下,想要使用传递的参数,则需要按位置把对应的参数取出来。尽管 arguments 是一个类数组且可遍历的变量,但它终究不是数组,它不支持数组方法,因此我们不能调用 arguments.forEeach (…) 等数组的方法。需要使用一些特殊的方法转换成数组使用,如:
function fn() { var arr = [].slice.call(arguments); console.log(arr) } fn('ES6'); // ["ES6"] fn('imooc', 7, 'ES6'); // ["imooc", 7, "ES6"]6)终于借助 call 方法把 arguments 转化成一个真正的数组了。但是这样无疑是一个繁琐的过程,而且不容易理解。这时 ES6 给出了它的完美解决方案 —— 剩余参数,那剩余参数是如何在函数传参中使用的呢?下面我们来看看实例:
语法:const add = (x, y, z, ...args) => {};function fn(...args) { console.log(args) } fn('ES6'); // ["ES6"] fn('imooc', 7, 'ES6'); // ["imooc", 7, "ES6"]7)使用方式很简单在函数定义时使用 … 紧接着跟一个收集的参数,这个收集的参数就是我们所传入不定参数的集合 —— 也就是数组。这样就很简单地摆脱了 arguments 的束缚。另外,还可以指定一个默认的参数,如下示例:
function fn(name, ...args) { console.log(name); // 基础参数 console.log(args); // 剩下的参数组成的数组 } fn('ES6'); // 'ES6' // [] fn('imooc', 7, 'ES6'); // "imooc" // [7, "ES6"]上面的代码中给函数第一个参数,声明一个变量 name,剩余的参数会被 … 收集成一个数组,这就是剩余参数。引入剩余参数就是为了能替代函数内部的 arguments,由于 arguments 对象不具备数组的方法,所以很多时候在使用之前要先转换成一个数组。而剩余参数本来就是一个数组,避免了这多余的一步,使用起来既优雅又自然
C.注意事项
1)箭头函数的剩余参数
箭头函数的参数部分即使只有一个剩余参数,也不能省略圆括号const add = (...args) => {};2)使用剩余参数替代 arguments 获取实际参数
- 剩余参数是一个 “真数组”,arguments 是一个 “伪数组”
- 剩余参数的名字可以自定义
3)剩余参数的位置
剩余参数只能是最后一个参数,之后不能再有其他参数,否则会报错
D.剩余参数的应用
1)作为数组的应用:const add = (...args) => { let sum = 0; for (let i = 0; i < args.length; i++) { sum += args[i]; } // 当然此处,arguments 也可以 return sum; }; console.log(add()); // 0 console.log(add(1, 1)); // 2 console.log(add(1, 2, 3)); // 62)与解构赋值结合使用:
(剩余参数不一定非要作为函数参数使用)- 与数组解构赋值结合
let array = [1, 2, 3, 4, 5]; let [a, b, ...others] = array; console.log(a); // 1 console.log(b); // 2 console.log(others); // [3,4,5]- 与对象解构赋值结合
const {x, y, ...z} = {a: 3, x: 1, y: 2, b: 4}; console.log(x, y, z); // 1 2 { a: 3, b: 4 } // 这里的剩余参数是个对象(准确的应该叫:剩余元素)const func = ({x, y, ...z}) => { console.log(x, y, z); // 1 2 { a: 3, b: 4 } }; func({a: 3, x: 1, y: 2, b: 4});- 在函数传参的时候也可以是和解构一起使用
function fun(...[a, b, c]) { return a + b + c; } fun('1') // NaN (b 和 c 都是 undefined) fun(1, 2, 3) // 6 fun(1, 2, 3, 4) // 6 多余的参数不会被获取到上面的代码中,a、b、c 会去解构传入参数,加上有剩余语法的作用,对应的值从数组中的项解构出来,在函数内部直接使用解构出来的参数即可。剩余语法看起来和展开语法完全相同,不同点在于,剩余参数用于解构数组和对象
E.小结 本节结合了 ES5 函数中的 arguments 对象引入了为什么 ES6 会引入剩余参数的概念,可以看到剩余参数所带来的好处。本节内容可以总结以下几点:
1)剩余参数是为了能替代函数内部的 arguments 而引入的;
2)和展开语法相反,剩余参数是将多个单个元素聚集起来形成一个单独的个体的过程
3.箭头函数
A.前言
1)在编程中使用最多的就是函数,在 ES5 中是用 function 关键字来定义函数的,由于历史原因 function 定义的函数存在一些问题,如 this 的指向、函数参数 arguments 等
2)ES6 规定了可以使用 “箭头”=>来定义一个函数,语法更加简洁。它没有自己的 this、arguments、super 或 new.target,箭头函数表达式更适用于那些本来需要匿名函数的地方,但它不能用作构造函数
B.认识箭头函数
1)普通函数:- function 函数名() {}
- () => {}
2)箭头函数:
- 参数 => 函数体
- const 变量名 = function () {};
由于箭头函数是匿名函数,所以我们通常把它赋给一个变量
const add = (x, y) => { return x + y; }; console.log(add(1, 1)); // 2C.箭头函数注意事项
1)省略写法const add = (x) => { return x + 1; }; // 单个参数可以省略 () const add = x => { return x + 1; }; // 无参数 const test = () => { return 1; }; //或者 const test = _ => { return 1; };2)单行函数体
const add = (x, y) => { return x + y; }; // 单行函数体可以省略 return 和 {},且一但省略就 return 和 {} 都要一起省略 const add = (x, y) => x + y;3)单行对象
const add = (x, y) => { return { value: x + y }; }; // const add = (x, y) => {value: x + y}; 报错!因为 {} 会产生歧义! // () 可以将语句变为表达式,从而 {} 就可以被顺理成章解释为对象 const add = (x, y) => ({value: x + y}); // 数组就没有以上问题 const add = (x, y) => [x, y];推荐:一般情况最好不要简写!
D.非箭头函数中的 this 指向
1)全局作用域中的 this 指向console.log(this); // window2)一般函数(非箭头函数)中的 this 指向
只有在函数调用的时候 this 指向才能确定,不调用的时候,不知道指向谁
this 指向和函数在哪儿没有关系,只和谁在调用有关function add() { console.log(this); } add(); // window // 在非严格模式下,this 其实是先指向 undefined,然后被自动转为了 windowfunction add() { console.log(this); } add(); // window // 在非严格模式下,this 其实是先指向 undefined,然后被自动转为了 windowfunction add() { console.log(this); } add(); // window // 在非严格模式下,this 其实是先指向 undefined,然后被自动转为了 windowE.箭头函数没有 this
1)在 JavaScript 中,要说让人最头疼的知识点中,this 绑定绝对算一个,这是因为 this 的绑定 ‘难以捉摸’,出错的时候还往往不知道为什么,相当反逻辑。下面我们来看一个示例:var title = "全局标题"; var imooc = { title: "慕课网 ES6 Wiki", getTitle : function(){ console.log(this.title); } }; imooc.getTitle(); // 慕课网 ES6 Wiki var bar = imooc.getTitle; bar(); // 全局标题通过上面的小例子的打印结果可以看出 this 的问题,说明 this 的指向是不固定的
2)这里简单说明一下 this 的指向,this 指向的是调用它的对象。例子中的 this 是在 getTitle 的函数中的,执行 imooc.getTitle() 这个方法时,调用它的对象是 imooc,所以 this 的指向是 imooc
之后把 imooc.getTitle 方法赋给 bar,这里要注意的是,只是把地址赋值给了 bar ,并没有调用。 而 bar 是全局对象 window 下的方法,所以在执行 bar 方法时,调用它的是 Window 对象,所以这里打印的结果是 window 下的 title——“全局标题”
TIPS: 上面的示例只是简单的 this 指向问题,还有很多更加复杂的,在面试中经常会被问到,所以还不清楚的同学可以去研究一下 this 的问题。
3)ES6 为了规避这样的问题,提出了箭头函数的解决方案,在箭头函数中没有自己的 this 指向,所有的 this 指向都指向它的上一层 this ,这样规定就比较容易理解了。下面看使用箭头函数下的 this 指向:var title = "全局标题"; var imooc = { title: "慕课网 ES6 Wiki", getTitle : () => { console.log(this.title); } }; imooc.getTitle(); // 全局标题 var bar = imooc.getTitle; bar(); // 全局标题上面的打印结果可以看出来,所有的 this 指向都指向了 window 对象下的 title,本身的 imooc 对象下没有了 this ,它的上一层就是 window
F.不适用箭头函数的场景
1)作为构造函数
因为箭头函数没有 this,而构造函数的核心就是 this
2)需要 this 指向调用对象的时候
因为箭头函数没有 this,所以如果箭头函数中出现了 this,那么这个 this 就是外层的!
3)需要使用 arguments 的时候
箭头函数没有 arguments
(这个问题有替代解决方案:剩余参数)var fun = function() { console.log(arguments) }; fun(1,2,3); // Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ] var fun = () => { console.log(arguments) }; fun(1,2,3); // Uncaught ReferenceError: arguments is not defined上面的示例中,对比两种定义函数的方法可以明显的看出,在箭头函数中去取 arguments 时会报引用错误,没有定义的 arguments
4)arguments 的主要作用是获取所有调用函数时所需要传入的参数,在箭头函数中使用剩余参数 …args,在函数内可以直接使用function foo(...args) { console.log(args) } foo(1); // [1] foo(1, 2, 3); // [1, 2, 3]G.其他注意点
1)不能用作构造器
箭头函数不能用作构造器,和 new 一起用会抛出错误var Foo = () => {}; var foo = new Foo(); // TypeError: Foo is not a constructor2)没有 prototype 属性
箭头函数没有 prototype 属性var Foo = () => {}; console.log(Foo.prototype); // undefined3)不能使用 yield 命令
yield 关键字通常不能在箭头函数中使用,因此箭头函数不能用作 Generator 函数
H.小结
1)本节主要讲解了 ES6 的箭头函数,总结了以下几点:- 更短的函数,优雅简洁;
- 箭头函数不会创建自己的 this,它只会从自己的作用域链的上一层继承 this;
- 不能绑定 arguments, 只能使用 ...args 展开运算来获取当前参数的数组
4.函数参数的尾逗号
1)ES2017 允许函数的最后一个参数有尾逗号(trailing comma)
此前,函数定义和调用时,都不允许最后一个参数后面出现逗号function clownsEverywhere( param1, param2 ) { /* ... */ } clownsEverywhere( 'foo', 'bar' );上面代码中,如果在param2或bar后面加一个逗号,就会报错
2)如果像上面这样,将参数写成多行(即每个参数占据一行),以后修改代码的时候,想为函数clownsEverywhere添加第三个参数,或者调整参数的次序,就势必要在原来最后一个参数后面添加一个逗号。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号function clownsEverywhere( param1, param2, ) { /* ... */ } clownsEverywhere( 'foo', 'bar', );这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了
5.catch 命令的参数省略
1)JavaScript 语言的try…catch结构,以前明确要求catch命令后面必须跟参数,接受try代码块抛出的错误对象try { // ... } catch (err) { // 处理错误 }上面代码中,catch命令后面带有参数err
2)很多时候,catch代码块可能用不到这个参数。但是,为了保证语法正确,还是必须写。ES2019做出了改变,允许catch语句省略参数try { // ... } catch { // ... }B5.数组的扩展
1.扩展运算符
A.含义
1)扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列console.log(...[1, 2, 3]) // 1 2 3 console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5 [...document.querySelectorAll('div')] // [<div>, <div>, <div>]2)该运算符主要用于函数调用
function push(array, ...items) { array.push(...items); } function add(x, y) { return x + y; } const numbers = [4, 38]; add(...numbers) // 42上面代码中,array.push(…items)和add(…numbers)这两行,都是函数的调用,它们都使用了扩展运算符。该运算符将一个数组,变为参数序列
3)扩展运算符与正常的函数参数可以结合使用,非常灵活function f(v, w, x, y, z) { } const args = [0, 1]; f(-1, ...args, 2, ...[3]);4)扩展运算符后面还可以放置表达式
const arr = [ ...(x > 0 ? ['a'] : []), 'b', ];5)如果扩展运算符后面是一个空数组,则不产生任何效果
[...[], 1] // [1]6)注意,只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错
(...[1, 2]) // Uncaught SyntaxError: Unexpected number console.log((...[1, 2])) // Uncaught SyntaxError: Unexpected number console.log(...[1, 2]) // 1 2上面三种情况,扩展运算符都放在圆括号里面,但是前两种情况会报错,因为扩展运算符所在的括号不是函数调用
B.替代函数的 apply() 方法
1)由于扩展运算符可以展开数组,所以不再需要apply()方法将数组转为函数的参数了// ES5 的写法 function f(x, y, z) { // ... } var args = [0, 1, 2]; f.apply(null, args); // ES6 的写法 function f(x, y, z) { // ... } let args = [0, 1, 2]; f(...args);2)下面是扩展运算符取代apply()方法的一个实际的例子,应用Math.max()方法,简化求出一个数组最大元素的写法
// ES5 的写法 Math.max.apply(null, [14, 3, 77]) // ES6 的写法 Math.max(...[14, 3, 77]) // 等同于 Math.max(14, 3, 77);上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用Math.max()函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用Math.max()了
3)另一个例子是通过push()函数,将一个数组添加到另一个数组的尾部// ES5 的写法 var arr1 = [0, 1, 2]; var arr2 = [3, 4, 5]; Array.prototype.push.apply(arr1, arr2); // ES6 的写法 let arr1 = [0, 1, 2]; let arr2 = [3, 4, 5]; arr1.push(...arr2);上面代码的 ES5 写法中,push()方法的参数不能是数组,所以只好通过apply()方法变通使用push()方法。有了扩展运算符,就可以直接将数组传入push()方法
4)下面是另外一个例子// ES5 new (Date.bind.apply(Date, [null, 2015, 1, 1])) // ES6 new Date(...[2015, 1, 1]);C.扩展运算符的应用
1)复制数组
数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组const a1 = [1, 2]; const a2 = a1; a2[0] = 2; a1 // [2, 2]上面代码中,a2并不是a1的克隆,而是指向同一份数据的另一个指针。修改a2,会直接导致a1的变化
2)ES5 只能用变通方法来复制数组const a1 = [1, 2]; const a2 = a1.concat(); a2[0] = 2; a1 // [1, 2]上面代码中,a1会返回原数组的克隆,再修改a2就不会对a1产生影响
3)扩展运算符提供了复制数组的简便写法const a1 = [1, 2]; // 写法 const a2 = [...a1];上面的两种写法,a2都是a1的克隆
4)合并数组
扩展运算符提供了数组合并的新写法const arr1 = ['a', 'b']; const arr2 = ['c']; const arr3 = ['d', 'e']; // ES5 的合并数组 arr1.concat(arr2, arr3); // [ 'a', 'b', 'c', 'd', 'e' ] // ES6 的合并数组 [...arr1, ...arr2, ...arr3] // [ 'a', 'b', 'c', 'd', 'e' ]5)不过,这两种方法都是浅拷贝,使用的时候需要注意
const a1 = [{ foo: 1 }]; const a2 = [{ bar: 2 }]; const a3 = a1.concat(a2); const a4 = [...a1, ...a2]; a3[0] === a1[0] // true a4[0] === a1[0] // true上面代码中,a3和a4是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了引用指向的值,会同步反映到新数组
6)字符串转为数组
扩展运算符还可以将字符串转为真正的数组console.log(...'alex'); // a l e x console.log('a', 'l', 'e', 'x'); // a l e x console.log([...'alex']); // [ 'a', 'l', 'e', 'x' ] // ES6 之前字符串转数组是通过:'alex'.split('');7)类数组转为数组
// arguments function func() { console.log(arguments); // [Arguments] { '0': 1, '1': 2 } console.log([...arguments]); // [ 1, 2 ] } func(1, 2); // NodeList console.log([...document.querySelectorAll('p')].push);2.Array.from()
A.前言
1)在前端开发中经常会遇到类数组,但是我们不能直接使用数组的方法,需要先把类数组转化为数组。本节介绍 ES6 数组的新增方法 Array.from(),该方法用于将类数组对象(array-like)和可遍历的对象(iterable)转换为真正的数组进行使用
B.方法详情
1)基本语法
Array.from() 方法会接收一个类数组对象然后返回一个真正的数组实例,返回的数组可以调用数组的所有方法
语法使用:Array.from(arrayLike[, mapFn[, thisArg]])2)参数解释:
参数 描述
arrayLike 想要转换成数组的类数组对象或可迭代对象
mapFn 如果指定了该参数,新数组中的每个元素会执行该回调函数
thisArg 可选参数,执行回调函数 mapFn 时 this 对象
3)类数组转化
所谓类数组对象,就是指可以通过索引属性访问元素,并且对象拥有 length 属性,类数组对象一般是以下这样的结构:var arrLike = { '0': 'apple', '1': 'banana', '2': 'orange', length: 3 };4)在 ES5 中没有对应的方法将类数组转化为数组,但是可以借助 call 和 apply 来实现:
var arr = [].slice.call(arrLike); // 或 var arr = [].slice.apply(arrLike);5)有了 ES6 的 Array.from() 就更简单了,对类数组对象直接操作,即可得到数组
var arr = Array.from(arrLike); console.log(arr) // ['apple', 'banana', 'orange']6)第二个参数 —— 回调函数
在 Array.from 中第二个参数是一个类似 map 函数的回调函数,该回调函数会依次接收数组中的每一项作为传入的参数,然后对传入值进行处理,最得到一个新的数组。 Array.from(obj, mapFn, thisArg) 也可以用 map 改写成这样 Array.from(obj).map(mapFn, thisArg)var arr = Array.from([1, 2, 3], function (x) { return 2 * x; }); var arr = Array.from([1, 2, 3]).map(function (x) { return 2 * x; }); //arr: [2, 4, 6]上面的例子展示了,Array.from 的参数可以使用 map 方法来进行替换,它们是等价的操作
7)第三个参数 ——this
Array.from 中第三个参数可以对回调函数中 this 的指向进行绑定,该参数是非常有用的,我们可以将被处理的数据和处理对象分离,将各种不同的处理数据的方法封装到不同的的对象中去,处理方法采用相同的名字
在调用 Array.from 对数据对象进行转换时,可以将不同的处理对象按实际情况进行注入,以得到不同的结果,适合解耦let obj = { handle: function(n){ return n + 2 } } Array.from([1, 2, 3, 4, 5], function (x){ return this.handle(x) }, obj) // [3, 4, 5, 6, 7]定义一个 obj 对象可以认作是,Array.from 回调函数中处理数据的方法集合,handle 是其中的一个方法,把 obj 作为第三个参数传给 Array.from 这样在回调函数中可以通过 this 来拿到 obj 对象
8)从字符串里生成数组
Array.from() 在传入字符串时,会把字符串的每一项都拆成单个的字符串作为数组中的一项Array.from('imooc'); // [ "i", "m", "o", "o", "c" ]9)从 Set 中生成数组
用 Set 定义的数组对象,可以使用 Array.from() 得到一个正常的数组const set = new Set(['a', 'b', 'c', 'd']); Array.from(set); // [ "a", "b", "c", "d" ]上面的代码中创建了一个 Set 数据结构,把实例传入 Array.from() 可以得到一个真正的数组
10)从 Map 中生成数组
Map 对象保存的是一个个键值对,Map 中的参数是一个数组或是一个可迭代的对象。 Array.from() 可以把 Map 实例转换为一个二维数组const map = new Map([[1, 2], [2, 4], [4, 8]]); Array.from(map); // [[1, 2], [2, 4], [4, 8]]C.使用案例
1)创建一个包含从 0 到 99 (n) 的连续整数的数组
一般情况下我们可以使用 for 循环来实现var arr = []; for(var i = 0; i <= 99; i++) { arr.push(i); }这种方法的主要优点是最直观了,性能也最好的,但是很多时候我们不想使用 for 循环来进行操作
2)使用 Array 配合 map 来实现var arr = Array(100).join(' ').split('').map(function(item,index){return index});Array (100) 创建了一个包含 100 个空位的数组,但是这样创建出来的数组是没法进行迭代的。所以要通过字符串转换,覆盖 undefined,最后调用 map 修改元素值
3)使用 es6 的 Array.from 实现使用 es6 的 Array.from 实现Array.from({length:100}) 可以定义一个可迭代的数组,数组的每一项都是 undefined,这样就非常方便的定义出所需要的数组了,但是这样定义的数组性能最差,具体可以参考 constArray 的测试结果
4)数组去重合并function combine(){ let arr = [].concat.apply([], arguments); //没有去重复的新数组 return Array.from(new Set(arr)); } var m = [1, 2, 2], n = [2,3,3]; console.log(combine(m,n)); // [1, 2, 3]首先定义一个去重数组函数,通过 concat 把传入的数组进行合并到一个新的数组中去,通过 new Set () 可以对 arr 进行去重操作,再使用 Array.from() 返回一个拷贝后的数组
D.小结
1)本节讲解了字符串的 Array.from() 方法的使用,用于将类数组对象和可迭代的对象转化真正的数组,在编程中主要用于更加方便的初始化一个有默认值的数组,还可以用于将获取的 html 的 DOM 对象转化为数组,可以使用数组方法进行操作
3.Array.of()
1)Array.of()方法用于将一组值,转换为数组Array.of(3, 11, 8) // [3,11,8] Array.of(3) // [3] Array.of(3).length // 12)这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异
Array() // [] Array(3) // [, , ,] Array(3, 11, 8) // [3, 11, 8]上面代码中,Array()方法没有参数、一个参数、三个参数时,返回的结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数只有一个正整数时,实际上是指定数组的长度
3)Array.of()基本上可以用来替代Array()或new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一Array.of() // [] Array.of(undefined) // [undefined] Array.of(1) // [1] Array.of(1, 2) // [1, 2]Array.of()总是返回参数值组成的数组。如果没有参数,就返回一个空数组
4)Array.of()方法可以用下面的代码模拟实现function ArrayOf(){ return [].slice.call(arguments); }4.find(),findIndex(),findLast(),findLastIndex()
1)数组实例的find()方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined[1, 4, -5, 10].find((n) => n < 0) // -5上面代码找出数组中第一个小于 0 的成员
2)[1, 5, 10, 15].find(function(value, index, arr) { return value > 9; }) // 10上面代码中,find()方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组
3)数组实例的findIndex()方法的用法与find()方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1[1, 5, 10, 15].findIndex(function(value, index, arr) { return value > 9; }) // 24)这两个方法都可以接受第二个参数,用来绑定回调函数的this对象
function f(v){ return v > this.age; } let person = {name: 'John', age: 20}; [10, 12, 26, 15].find(f, person); // 26上面的代码中,find()函数接收了第二个参数person对象,回调函数中的this对象指向person对象
5)另外,这两个方法都可以发现NaN,弥补了数组的indexOf()方法的不足[NaN].indexOf(NaN) // -1 [NaN].findIndex(y => Object.is(NaN, y)) // 0上面代码中,indexOf()方法无法识别数组的NaN成员,但是findIndex()方法可以借助Object.is()方法做到
6)find()和findIndex()都是从数组的0号位,依次向后检查。ES2022 新增了两个方法findLast()和findLastIndex(),从数组的最后一个成员开始,依次向前检查,其他都保持不变const array = [ { value: 1 }, { value: 2 }, { value: 3 }, { value: 4 } ]; array.findLast(n => n.value % 2 === 1); // { value: 3 } array.findLastIndex(n => n.value % 2 === 1); // 2上面示例中,findLast()和findLastIndex()从数组结尾开始,寻找第一个value属性为奇数的成员。结果,该成员是{ value: 3 },位置是2号位
5.filter()
1)filter()方法用于过滤数组成员,满足条件的成员组成一个新数组返回
它的参数是一个函数,所有数组成员依次执行该函数,返回结果为true的成员组成一个新数组返回。该方法不会改变原数组[1, 2, 3, 4, 5].filter(function (elem) { return (elem > 3); }) // [4, 5]上面代码将大于3的数组成员,作为一个新数组返回
2)var arr = [0, 1, 'a', false]; arr.filter(Boolean) // [1, "a"]上面代码中,filter()方法返回数组arr里面所有布尔值为true的成员
3)filter()方法的参数函数可以接受三个参数:当前成员,当前位置和整个数组[1, 2, 3, 4, 5].filter(function (elem, index, arr) { return index % 2 === 0; }); // [1, 3, 5]上面代码返回偶数位置的成员组成的新数组
4)filter()方法还可以接受第二个参数,用来绑定参数函数内部的this变量var obj = { MAX: 3 }; var myFilter = function (item) { if (item > this.MAX) return true; }; var arr = [2, 8, 3, 4, 1, 3, 2, 9]; arr.filter(myFilter, obj) // [8, 4, 9]上面代码中,过滤器myFilter()内部有this变量,它可以被filter()方法的第二个参数obj绑定,返回大于3的成员
6.map()
1)map()方法将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回var numbers = [1, 2, 3]; numbers.map(function (n) { return n + 1; }); // [2, 3, 4] numbers // [1, 2, 3]上面代码中,numbers数组的所有成员依次执行参数函数,运行结果组成一个新数组返回,原数组没有变化
2)map()方法接受一个函数作为参数。该函数调用时,map()方法向它传入三个参数:当前成员、当前位置和数组本身[1, 2, 3].map(function(elem, index, arr) { return elem * index; }); // [0, 2, 6]上面代码中,map()方法的回调函数有三个参数,elem为当前成员的值,index为当前成员的位置,arr为原数组([1, 2, 3])
3)map()方法还可以接受第二个参数,用来绑定回调函数内部的this变量(详见《this 变量》一章)var arr = ['a', 'b', 'c']; [1, 2].map(function (e) { return this[e]; }, arr) // ['b', 'c']上面代码通过map()方法的第二个参数,将回调函数内部的this对象,指向arr数组
4)如果数组有空位,map()方法的回调函数在这个位置不会执行,会跳过数组的空位var f = function (n) { return 'a' }; [1, undefined, 2].map(f) // ["a", "a", "a"] [1, null, 2].map(f) // ["a", "a", "a"] [1, , 2].map(f) // ["a", , "a"]上面代码中,map()方法不会跳过undefined和null,但是会跳过空位
7.reduce()
1)reduce()方法依次处理数组的每个成员,最终累计为一个值。它们的差别是,reduce()是从左到右处理(从第一个成员到最后一个成员)
语法:arr.reduce(function(累计值, 当前元素){}, 起始值)[1, 2, 3, 4, 5].reduce(function (a, b) { console.log(a, b); return a + b; }) // 1 2 // 3 3 // 6 4 // 10 5 //最后结果:15上面代码中,reduce()方法用来求出数组所有成员的和。reduce()的参数是一个函数,数组每个成员都会依次执行这个函数。如果数组有 n 个成员,这个参数函数就会执行 n - 1 次
- 第一次执行:a是数组的第一个成员1,b是数组的第二个成员2
- 第二次执行:a为上一轮的返回值3,b为第三个成员3
- 第三次执行:a为上一轮的返回值6,b为第四个成员4
- 第四次执行:a为上一轮返回值10,b为第五个成员5。至此所有成员遍历完成,整个方法的返回值就是最后一轮的返回值15
2)reduce()方法的第一个参数都是一个函数。该函数接受以下四个参数
a.累积变量。第一次执行时,默认为数组的第一个成员;以后每次执行时,都是上一轮的返回值
b.当前变量。第一次执行时,默认为数组的第二个成员;以后每次执行时,都是下一个成员
c.当前位置。一个整数,表示第二个参数(当前变量)的位置,默认为1
d.原数组
3)这四个参数之中,只有前两个是必须的,后两个则是可选的[1, 2, 3, 4, 5].reduce(function ( a, // 累积变量,必须 b, // 当前变量,必须 i, // 当前位置,可选 arr // 原数组,可选 ) { // ... ...4)如果要对累积变量指定初值,可以把它放在reduce()方法的第二个参数
[1, 2, 3, 4, 5].reduce(function (a, b) { return a + b; }, 10); // 25上面代码指定参数a的初值为10,所以数组从10开始累加,最终结果为25。注意,这时b是从数组的第一个成员开始遍历,参数函数会执行5次
5)建议总是加上第二个参数,这样比较符合直觉,每个数组成员都会依次执行reduce()方法的参数函数。另外,第二个参数可以防止空数组报错function add(prev, cur) { return prev + cur; } [].reduce(add) // TypeError: Reduce of empty array with no initial value [].reduce(add, 1) // 1上面代码中,由于空数组取不到累积变量的初始值,reduce()方法会报错。这时,加上第二个参数,就能保证总是会返回一个值
6)总结//reduce 返回函数累计处理的结果,经常用于求和等 /* 计值参数: 1. 如果有起始值,则以起始值为准开始累计, 累计值 = 起始值 2. 如果没有起始值, 则累计值以数组的第一个数组元素作为起始值开始累计 3. 后面每次遍历就会用后面的数组元素 累计到 累计值 里面(类似求和里面的 sum ) */8.some(),every()
1)这两个方法类似“断言”(assert),返回一个布尔值,表示判断数组成员是否符合某种条件
2)some方法是只要一个成员的返回值是true,则整个some方法的返回值就是true,否则返回falsevar arr = [1, 2, 3, 4, 5]; arr.some(function (elem, index, arr) { return elem >= 3; }); // true上面代码中,如果数组arr有一个成员大于等于3,some方法就返回true
3)every方法是所有成员的返回值都是true,整个every方法才返回true,否则返回falsevar arr = [1, 2, 3, 4, 5]; arr.every(function (elem, index, arr) { return elem >= 3; }); // false上面代码中,数组arr并非所有成员大于等于3,所以返回false
4)注意,对于空数组,some方法返回false,every方法返回true,回调函数都不会执行function isEven(x) { return x % 2 === 0 } [].some(isEven) // false [].every(isEven) // truesome和every方法还可以接受第二个参数,用来绑定参数函数内部的this变量
9.fill()
1)arr.fill(value[, start[, end]])方法用一个固定值填充一个数组中从起始索引到终止索引内的全部元素。不包括终止索引。 起始索引,默认值为 0。 终止索引,默认值为 this.length['a', 'b', 'c'].fill(7) // [7, 7, 7] new Array(3).fill(7) // [7, 7, 7]上面代码表明,fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去
2)fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c']上面代码表示,fill方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束
3)注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象let arr = new Array(3).fill({name: "Mike"}); arr[0].name = "Ben"; arr // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}] let arr = new Array(3).fill([]); arr[0].push(5); arr // [[5], [5], [5]]10.at()
1)长久以来,JavaScript 不支持数组的负索引,如果要引用数组的最后一个成员,不能写成arr[-1],只能使用arr[arr.length - 1]
2)这是因为方括号运算符[]在 JavaScript 语言里面,不仅用于数组,还用于对象。对于对象来说,方括号里面就是键名,比如obj[1]引用的是键名为字符串1的键,同理obj[-1]引用的是键名为字符串-1的键。由于 JavaScript 的数组是特殊的对象,所以方括号里面的负数无法再有其他语义了,也就是说,不可能添加新语法来支持负索引
3)这是因为方括号运算符[]在 JavaScript 语言里面,不仅用于数组,还用于对象。对于对象来说,方括号里面就是键名,比如obj[1]引用的是键名为字符串1的键,同理obj[-1]引用的是键名为字符串-1的键。由于 JavaScript 的数组是特殊的对象,所以方括号里面的负数无法再有其他语义了,也就是说,不可能添加新语法来支持负索引const arr = [5, 12, 8, 130, 44]; arr.at(2) // 8 arr.at(-2) // 1304)如果参数位置超出了数组范围,at()返回undefined
const sentence = 'This is a sample sentence'; sentence.at(0); // 'T' sentence.at(-1); // 'e' sentence.at(-100) // undefined sentence.at(100) // undefined11.entries(),keys() 和 values()
1)entries(),keys() 和 values()for (let index of ['a', 'b'].keys()) { console.log(index); } // 0 // 1 for (let elem of ['a', 'b'].values()) { console.log(elem); } // 'a' // 'b' for (let [index, elem] of ['a', 'b'].entries()) { console.log(index, elem); } // 0 "a" // 1 "b"12.includes()
1)Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016 引入了该方法[1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false [1, 2, NaN].includes(NaN) // true2)该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始
[1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true3)没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值
if (arr.indexOf(el) !== -1) { // ... }4)indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判
[NaN].indexOf(NaN) // -15)includes使用的是不一样的判断算法,就没有这个问题
[NaN].includes(NaN) // true6)另外,Map 和 Set 数据结构有一个has方法,需要注意与includes区分
- Map 结构的has方法,是用来查找键名的,比如Map.prototype.has(key)、WeakMap.prototype.has(key)、Reflect.has(target, propertyKey)
- Set 结构的has方法,是用来查找值的,比如Set.prototype.has(value)、WeakSet.prototype.has(value)
13.toReversed(),toSorted(),toSpliced(),with()
1)很多数组的传统方法会改变原数组,比如push()、pop()、shift()、unshift()等等。数组只要调用了这些方法,它的值就变了。现在有一个提案,允许对数组进行操作时,不改变原数组,而返回一个原数组的拷贝
2)这样的方法一共有四个- Array.prototype.toReversed() -> Array
- Array.prototype.toSorted(compareFn) -> Array
- Array.prototype.toSpliced(start, deleteCount, ...items) -> Array
- Array.prototype.with(index, value) -> Array
3)它们分别对应数组的原有方法
- toReversed()对应reverse(),用来颠倒数组成员的位置
- toSorted()对应sort(),用来对数组成员排序
- toSpliced()对应splice(),用来在指定位置,删除指定数量的成员,并插入新成员
- with(index, value)对应splice(index, 1, value),用来将指定位置的成员替换为新的值
上面是这四个新方法对应的原有方法,含义和用法完全一样,唯一不同的是不会改变原数组,而是返回原数组操作后的拷贝
4)下面是示例const sequence = [1, 2, 3]; sequence.toReversed() // [3, 2, 1] sequence // [1, 2, 3] const outOfOrder = [3, 1, 2]; outOfOrder.toSorted() // [1, 2, 3] outOfOrder // [3, 1, 2] const array = [1, 2, 3, 4]; array.toSpliced(1, 2, 5, 6, 7) // [1, 5, 6, 7, 4] array // [1, 2, 3, 4] const correctionNeeded = [1, 1, 3]; correctionNeeded.with(1, 2) // [1, 2, 3] correctionNeeded // [1, 1, 3]14.isArray()
A.前言
1)在程序中判断数组是很常见的应用,但在 ES5 中没有能严格判断 JS 对象是否为数组,都会存在一定的问题,比较受广大认可的是借助 toString 来进行判断,很显然这样不是很简洁。ES6 提供了 Array.isArray() 方法更加简洁地判断 JS 对象是否为数组
B.方法详情
1)判断 JS 对象,如果值是 Array,则为 true; 否则为 false
语法使用:Array.isArray(obj)2)参数解释:
参数 描述
obj 需要检测的 JS 对象
C.ES5 中判断数组的方法
1)通常使用 typeof 来判断变量的数据类型,但是对数组得到不一样的结果// 基本类型 typeof 123; //number typeof "123"; //string typeof true; //boolean // 引用类型 typeof [1,2,3]; //object上面的代码中,对于基本类型的判断没有问题,但是判断数组时,返回了 object 显然不能使用 typeof 来作为判断数组的方法
2)通过 instanceof 判断
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链
instanceof 可以用来判断数组是否存在,判断方式如下:var arr = ['a', 'b', 'c']; console.log(arr instanceof Array); // true console.log(arr.constructor === Array;); // true3)在解释上面的代码时,先看下数组的原型链指向示意图:

数组实例的原型链指向的是 Array.prototype 属性,instanceof 运算符就是用来检测 Array.prototype 属性是否存在于数组的原型链上,上面代码中的 arr 变量就是一个数组,所有拥有 Array.prototype 属性,返回值 true,这样就很好的判断数组类型了
但是,需要注意的是,prototype 属性是可以修改的,所以并不是最初判断为 true 就一定永远为真
4)通过 constructor 判断
我们知道,Array 是 JavaScript 内置的构造函数,构造函数属性(prototype)的 constructor 指向构造函数(见下图),那么通过 constructor 属性也可以判断是否为一个数组var arr = new Array('a', 'b', 'c'); arr.constructor === Array; //true5)下面我们通过构造函数的示意图来进行分析:

由上面的示意图可以知道,我们 new 出来的实例对象上的原型对象有 constructor 属性指向构造函数 Array,由此我们可以判断一个数组类型
6)但是 constructor 是可以被重写,所以不能确保一定是数组,如下示例:var str = 'abc'; str.constructor = Array; str.constructor === Array // true上面的代码中,str 显然不是数组,但是可以把 constructor 指向 Array 构造函数,这样再去进行判断就是有问题的了
constructor 和 instanceof 也存在同样问题,不同执行环境下,constructor 的判断也有可能不正确
D.Array.isArray () 的使用
1)下面我们通过示例来看下 Array.isArray() 是怎样判断数组的// 下面的函数调用都返回 true Array.isArray([]); Array.isArray([10]); Array.isArray(new Array()); Array.isArray(new Array('a', 'b', 'c')) // 鲜为人知的事实:其实 Array.prototype 也是一个数组。 Array.isArray(Array.prototype); // 下面的函数调用都返回 false Array.isArray(); Array.isArray({}); Array.isArray(null); Array.isArray(undefined); Array.isArray(17); Array.isArray('Array'); Array.isArray(true); Array.isArray(false); Array.isArray(new Uint8Array(32)) Array.isArray({ __proto__: Array.prototype });上面的代码中对 JavaScript 中的数据类型做验证,可以很好地区分数组类型
E.自定义 isArray
1)在 ES5 中比较通用的方法是使用 Object.prototype.toString 去判断一个值的类型,也是各大主流库的标准。在不支持 ES6 语法的环境下可以使用下面的方法给 Array 上添加 isArray 方法if (!Array.isArray){ Array.isArray = function(arg){ return Object.prototype.toString.call(arg) === '[object Array]'; }; }F.小结
1)本节介绍了判断一个值是数组类型的方法 Array.isArray() 此方法可以很准确地判断数组,学习了在 ES5 中判断数组类型的几个方法的缺陷。在不支持 ES6 的情况下也可以通过 Object.prototype.toString 自定义 Array.isArray() 方法B6.对象的扩展
1.属性的简洁表示法
1)ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁const foo = 'bar'; const baz = {foo}; baz // {foo: "bar"} // 等同于 const baz = {foo: foo};上面代码中,变量foo直接写在大括号里面。这时,属性名就是变量名, 属性值就是变量值。下面是另一个例子
2)function f(x, y) { return {x, y}; } // 等同于 function f(x, y) { return {x: x, y: y}; } f(1, 2) // Object {x: 1, y: 2}3)除了属性简写,方法也可以简写
const o = { method() { return "Hello!"; } }; // 等同于 const o = { method: function() { return "Hello!"; } };4)下面是一个实际的例子
let birth = '2000/01/01'; const Person = { name: '张三', //等同于birth: birth birth, // 等同于hello: function ()... hello() { console.log('我的名字是', this.name); } };5)这种写法用于函数的返回值,将会非常方便
function getPoint() { const x = 1; const y = 10; return {x, y}; } getPoint() // {x:1, y:10}2.方括号语法
A.方括号语法的用法const prop = 'age'; const person = {}; person.prop = 18; console.log(person); // { prop: 18 } // ----------------------------------------- const prop = 'age'; const person = {}; person[prop] = 18; console.log(person); // { age: 18 } // ----------------------------------------- // ES6 增强 const prop = 'age'; const person = { [prop]: 18 }; console.log(person); // { age: 18 }B.方括号中可以放什么
// [值、可以得到值的表达式] const prop = 'age'; const func = () => 'age2'; const person = { [prop]: 18, [func()]: 24, ['sex']: 'man', ['s' + 'ex2']: 'womam' }; console.log(person); // { age: 18, age2: 24, sex: 'man', sex2: 'womam' }1)注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object],这一点要特别小心
const keyA = {a: 1}; const keyB = {b: 2}; const myObject = { [keyA]: 'valueA', [keyB]: 'valueB' }; myObject // Object {[object Object]: "valueB"}上面代码中,[keyA]和[keyB]得到的都是[object Object],所以[keyB]会把[keyA]覆盖掉,而myObject最后只有一个[object Object]属性
C.方括号语法和点语法的区别
1)点语法是方括号语法的特殊形式
2)属性名由数字、字母、下划线以及 $ 构成,并且数字还不能打头的时候可以使用点语法(合法标识符)
3)能用点语法优先使用点语法const person = { age: 18 }; person.age 等价于 person['age']3.super 关键字
1)我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象const proto = { foo: 'hello' }; const obj = { foo: 'world', find() { return super.foo; } }; Object.setPrototypeOf(obj, proto); obj.find() // "hello"上面代码中,对象obj.find()方法之中,通过super.foo引用了原型对象proto的foo属性
2)注意,super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错// 报错 const obj = { foo: super.foo } // 报错 const obj = { foo: () => super.foo } // 报错 const obj = { foo: function () { return super.foo } }上面三种super的用法都会报错,因为对于 JavaScript 引擎来说,这里的super都没有用在对象的方法之中。第一种写法是super用在属性里面,第二种和第三种写法是super用在一个函数里面,然后赋值给foo属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法
3)JavaScript 引擎内部,super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)const proto = { x: 'hello', foo() { console.log(this.x); }, }; const obj = { x: 'world', foo() { super.foo(); } } Object.setPrototypeOf(obj, proto); obj.foo() // "world"上面代码中,super.foo指向原型对象proto的foo方法,但是绑定的this却还是当前对象obj,因此输出的就是world
4.对象的展开运算符
A.展开对象
1)对象不能直接展开,必须在 {} 中展开const apple = { color: '红色', shape: '球形', taste: '甜' }; console.log({...apple}); // { color: '红色', shape: '球形', taste: '甜' } console.log({...apple} === apple); // falseB.合并对象
const apple = { color: '红色', shape: '球形', taste: '甜' }; const pen = { color: '黑色', shape: '圆柱形', use: '写字' }; // 新对象拥有全部属性,相同属性,后者覆盖前者 console.log({...apple, ...pen}); // { color: '黑色', shape: '圆柱形', taste: '甜', use: '写字' } console.log({...pen, ...apple}); // { color: '红色', shape: '球形', use: '写字', taste: '甜' }C.注意事项
1)空对象的展开
如果展开一个空对象,则没有任何效果console.log({...{}}); // {} console.log({...{}, a: 1}); // { a: 1 }2)非对象的展开
如果展开的不是对象,则会自动将其转为对象,再将其属性罗列出来(没有属性便为空)console.log({...1}); // {} console.log(new Object(1)); // [Number: 1] console.log({...undefined}); // {} console.log({...null}); // {} console.log({...true}); // {}3)字符串的展开
如果展开运算符后面是字符串,它会自动转成一个类似数组的对象,因此返回的不是空对象// 字符串在对象中展开 console.log({...'alex'}); // { '0': 'a', '1': 'l', '2': 'e', '3': 'x' } // 字符串在数组中展开 console.log([...'alex']); // [ 'a', 'l', 'e', 'x' ] // 字符串直接展开 console.log(...'alex'); // a l e x4)数组的展开
console.log({...[1, 2, 3]}); // { '0': 1, '1': 2, '2': 3 }5)对象中对象属性的展开
不会展开对象中的对象属性const apple = { feature: { taste: '甜' } }; const pen = { feature: { color: '黑色', shape: '圆柱形' }, use: '写字' }; console.log({...apple}); // { feature: { taste: '甜' } } // feature 会直接覆盖,因为 feature 不能展开 console.log({...apple, ...pen}); // { feature: { color: '黑色', shape: '圆柱形' }, use: '写字' }D.对象展开运算符的应用
1)复制对象const a = {x: 1, y: 2}; const c = {...a}; console.log(c, c === a); // { x: 1, y: 2 } false2)用户参数和默认参数
const logUser = userParam => { const defaultPeram = { username: 'ZhangSan', age: 0, sex: 'male' }; const param = {...defaultPeram, ...userParam}; console.log(param.username, param.age, param.sex); }; logUser({username: 'jerry'}); // jerry 0 male3)再优化:
const logUser = userParam => { const defaultPeram = { username: 'ZhangSan', age: 0, sex: 'male' }; const {username, age, sex} = {...defaultPeram, ...userParam}; console.log(username, age, sex); }; logUser({username: 'jerry'}); // jerry 0 male5.对象的新增方法
A.Object.is()
1)ES5 比较两个值是否相等,只有两个运算符:相等运算符(= =)和严格相等运算符 (= = =)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等
ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致Object.is('foo', 'foo') // true Object.is({}, {}) // false2)不同之处只有两个:一是+0不等于-0,二是NaN等于自身
+0 === -0 //true NaN === NaN // false Object.is(+0, -0) // false Object.is(NaN, NaN) // trueB.Object.assign()
1)Object.assign()方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)const target = { a: 1 }; const source1 = { b: 2 }; const source2 = { c: 3 }; Object.assign(target, source1, source2); target // {a:1, b:2, c:3}2)汇总
// 基本用法 // Object.assign(目标对象, 源对象1, 源对象2, ...); const apple = { color: '红色', shape: '圆形', taste: '甜' }; const pen = { color: '黑色', shape: '圆柱形', use: '写字' }; console.log(Object.assign(apple, pen)); // 后面的覆盖前面的(最终返回的不是新的,而是修改了前面的) // { color: '黑色', shape: '圆柱形', taste: '甜', use: '写字' } // Object.assign 直接合并到了第一个参数中,返回的就是合并后的对象 console.log(apple); // { color: '黑色', shape: '圆柱形', taste: '甜', use: '写字' } console.log(Object.assign(apple, pen) === apple); // true // 可以合并多个对象 // 第一个参数使用一个空对象来实现合并返回一个新对象的目的 console.log(Object.assign({}, apple, pen)); // { color: '黑色', shape: '圆柱形', taste: '甜', use: '写字' } console.log(apple); // { color: '红色', shape: '圆形', taste: '甜' } console.log({...apple, ...pen}); // { color: '黑色', shape: '圆柱形', taste: '甜', use: '写字' } // 注意事项 // (1) 基本数据类型作为源对象 // 与对象的展开类似,先转换成对象,再合并 console.log(Object.assign({}, undefined)); // {} console.log(Object.assign({}, null)); // {} console.log(Object.assign({}, 1)); // {} console.log(Object.assign({}, true)); // {} console.log(Object.assign({}, 'str')); // { '0': 's', '1': 't', '2': 'r' } // (2) 同名属性的替换 // 后面的直接覆盖前面的 const apple = { color: ['红色', '黄色'], shape: '圆形', taste: '甜' }; const pen = { color: ['黑色', '银色'], shape: '圆柱形', use: '写字' }; console.log(Object.assign({}, apple, pen)); // { color: [ '黑色', '银色' ], shape: '圆柱形', taste: '甜', use: '写字' } // 应用 // 合并默认参数和用户参数 const logUser = userOptions => { const DEFAULTS = { username: 'ZhangSan', age: 0, sex: 'male' }; const options = Object.assign({}, DEFAULTS, userOptions); console.log(options); }; logUser(); // { username: 'ZhangSan', age: 0, sex: 'male' } logUser({}); // { username: 'ZhangSan', age: 0, sex: 'male' } logUser({username: 'Alex'}); // { username: 'Alex', age: 0, sex: 'male' }C.Object.keys()、Object.values() 和 Object.entries()
1)Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名
2)Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值
3)Object.entries()方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组// 基本用法 const person = { name: 'Alex', age: 18 }; // 返回键数组 console.log(Object.keys(person)); // [ 'name', 'age' ] // 返回值数组 console.log(Object.values(person)); // [ 'Alex', 18 ] // 返回键值二维数组 console.log(Object.entries(person)); // [ [ 'name', 'Alex' ], [ 'age', 18 ] ] // 与数组类似方法的区别 console.log([1, 2].keys()); // Object [Array Iterator] {} console.log([1, 2].values()); // Object [Array Iterator] {} console.log([1, 2].entries()); // Object [Array Iterator] {} // 数组的 keys()、values()、entries() 等方法是实例方法,返回的都是 Iterator // 对象的 Object.keys()、Object.values()、Object.entries() 等方法是构造函数方法,返回的是数组 // 应用(使用 for...of 循环遍历对象) const person = { name: 'Alex', age: 18 }; for (const key of Object.keys(person)) { console.log(key); } // name // age for (const value of Object.values(person)) { console.log(value); } // Alex // 18 for (const entries of Object.entries(person)) { console.log(entries); } // [ 'name', 'Alex' ] // [ 'age', 18 ] for (const [key, value] of Object.entries(person)) { console.log(key, value); } // name Alex // age 18 // Object.keys()/values()/entires() 并不能保证顺序一定是你看到的样子,这一点和 for in 是一样的 // 如果对遍历顺序有要求那么不能用 for in 以及这种方法,而要用其他方法B7.字符串的扩展
1.模板字符串
A.认识模板字符串
1)普通字符串:'字符串' "字符串"2)模板字符串:
`字符串`B.模板字符串与一般字符串的区别
1)对于普通用法没有区别const name1 = 'zjr'; const name2 = `zjr`; console.log(name1, name2, name1 === name2); // zjr zjr true2)字符串拼接的巨大区别
const person = { name: 'zjr', age: 18, sex: '男' }; const info = '我的名字是:' + person.name + ',性别是:' + person.sex + ',今年:' + person.age + '岁'; console.log(info); // 我的名字是:zjr,性别是:男,今年:18岁const person = { name: `zjr`, age: 18, sex: `男` }; const info = `我的名字是:${person.name},性别是:${person.sex},今年:${person.age}岁`; console.log(info); // 我的名字是:zjr,性别是:male,今年:18岁模板字符串最大的优势:方便注入!
C.模板字符串的注意事项
1)输出多行字符串// 一般字符串 const info = '第一行\n第二行'; console.log(info); /* 第一行 第二行 */ // 模板字符串 const info = `第一行 第二行`; // 注意不能有缩进 console.log(info); /* 第一行 第二行 */模板字符串中,所有的空格、换行或缩进都会被保存在输出中
2)输出 `` 和` 等特殊字符const info = `\``; // ``` const info = `\\`; // `\` const info = `""`; // `""` const info = `''`; // `''`3)模板字符串的注入
const username = 'alex'; const person = { age: 18, sex: `male` }; const getSex = function (sex) { return sex === `male` ? '男' : '女'; }; const info = `${username},${person.age + 2},${getSex(person.sex)}`; console.log(info); // alex,20,男模板字符串的 ${} 注入可以兼容几乎所有的值!
模板字符串、字符串、数值、布尔值、表达式、函数……(只要结果是个 “值” 即可)
D.模板字符串的应用
1)<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"/> <title>模板字符串的应用</title> <style> body { padding: 50px 0 0 300px; font-size: 22px; } ul { padding: 0; } p { margin-bottom: 10px; } </style> </head> <body> <p>学生信息表</p> <ul id="list"> <li style="list-style: none;">信息加载中……</li> </ul> <script> // 数据(此处只是模拟数据,后期是通过 Ajax 从后台获取) const students = [ { username: 'Alex', age: 18, sex: 'male' }, { username: 'ZhangSan', age: 28, sex: 'male' }, { username: 'LiSi', age: 20, sex: 'female' } ]; const list = document.getElementById('list'); let html = ''; for (let i = 0; i < students.length; i++) { html += `<li>我的名字是:${students[i].username},${students[i].sex},${students[i].age}</li>`; } list.innerHTML = html; </script> </body> </html>
2.includes(), startsWith(), endsWith()
A.传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法
1)includes():返回布尔值,表示是否找到了参数字符串
2)startsWith():返回布尔值,表示参数字符串是否在原字符串的头部
3)endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部let s = 'Hello world!'; s.startsWith('Hello') // true s.endsWith('!') // true s.includes('o') // true4)这三个方法都支持第二个参数,表示开始搜索的位置
上面代码表示,使用第二个参数n时,endsWith的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束
3.repeat()
1)repeat方法返回一个新字符串,表示将原字符串重复n次'x'.repeat(3) // "xxx" 'hello'.repeat(2) // "hellohello" 'na'.repeat(0) // ""2)参数如果是小数,会被取整
'na'.repeat(2.9) // "nana"3)如果repeat的参数是负数或者Infinity,会报错
'na'.repeat(Infinity) // RangeError 'na'.repeat(-1) // RangeError4)但是,如果参数是 0 到-1 之间的小数,则等同于 0,这是因为会先进行取整运算。0 到-1 之间的小数,取整以后等于-0,repeat视同为 0
'na'.repeat(-0.9) // ""5)参数NaN等同于 0
'na'.repeat(NaN) // ""6)如果repeat的参数是字符串,则会先转换成数字
'na'.repeat('na') // "" 'na'.repeat('3') // "nanana"4.padStart(),padEnd()
1)ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全'x'.padStart(5, 'ab') // 'ababx' 'x'.padStart(4, 'ab') // 'abax' 'x'.padEnd(5, 'ab') // 'xabab' 'x'.padEnd(4, 'ab') // 'xaba'上面代码中,padStart()和padEnd()一共接受两个参数,第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串
2)如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串'xxx'.padStart(2, 'ab') // 'xxx' 'xxx'.padEnd(2, 'ab') // 'xxx'3)如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串
'abc'.padStart(10, '0123456789') // '0123456abc'4)如果省略第二个参数,默认使用空格补全长度
'x'.padStart(4) // ' x' 'x'.padEnd(4) // 'x '5)padStart()的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串
'1'.padStart(10, '0') // "0000000001" '12'.padStart(10, '0') // "0000000012" '123456'.padStart(10, '0') // "0000123456"6)另一个用途是提示字符串格式
'12'.padStart(10, 'YYYY-MM-DD') // "YYYY-MM-12" '09-12'.padStart(10, 'YYYY-MM-DD') // "YYYY-09-12"5.trimStart(),trimEnd()
1)trimStart()和trimEnd()这两个方法,它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串const s = ' abc '; s.trim() // "abc" s.trimStart() // "abc " s.trimEnd() // " abc"上面代码中,trimStart()只消除头部的空格,保留尾部的空格。trimEnd()也是类似行为
除了空格键,这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效
浏览器还部署了额外的两个方法,trimLeft()是trimStart()的别名,trimRight()是trimEnd()的别名
6.at()
1)at()方法接受一个整数作为参数,返回参数指定位置的字符,支持负索引(即倒数的位置)const str = 'hello'; str.at(1) // "e" str.at(-1) // "o"如果参数位置超出了字符串范围,at()返回undefined
B8.Set 和 Map 数据结构
1.什么是 Set?
1)Set 是一系列无序、没有重复值的数据集合
数组是一系列有序(下标索引)的数据集合const s = new Set(); s.add(1); s.add(2); // Set 中不能有重复的成员 s.add(1); console.log(s); // Set(2) { 1, 2 } // Set 没有下标去标识每一个值,所以 Set 是无序的,也不能像数组那样通过下标去访问 Set 的成员。2.Set 实例的方法和属性
A.add 方法const s = new Set(); s.add(0); // 可以连写 s.add(1).add(2).add(2).add(3); console.log(s); // Set(4) { 0, 1, 2, 3 }B.
has 方法C.delete 方法
const s = new Set(); s.add(0); s.add(1).add(2).add(2).add(3); s.delete(2); // 使用 delete 删除不存在的成员,什么都不会发生,也不会报错 s.delete(4); console.log(s); // Set(3) { 0, 1, 3 }D.clear 方法
const s = new Set(); s.add(0); s.add(1).add(2).add(2).add(3); s.clear(); console.log(s); // Set(0) {}E.forEach 方法
作用:用于遍历 Set 的(按照成员添加进集合的顺序遍历)
forEach 方法可以接受两个参数,第一个是:回调函数,第二个是:指定回调函数的 this 指向const s = new Set(); s.add(0); s.add(1).add(2).add(2).add(3); s.forEach(function (value, key, set) { // Set 中 value = key,原因:好多数据结构都有 forEach 方法,为了方便统一,所以参数是统一的,但是参数的意义各有不同 // set 就是 s 本身 console.log(value, key, set === s); console.log(this); }); /* 0 0 true Window 1 1 true Window 2 2 true Window 3 3 true Window */const s = new Set(); s.add(0); s.add(1).add(2).add(2).add(3); s.forEach(function (value, key, set) { // Set 中 value = key,原因:好多数据结构都有 forEach 方法,为了方便统一,所以参数是统一的,但是参数的意义各有不同 // set 就是 s 本身 console.log(value, key, set === s); console.log(this); }, document); /* 0 0 true #document 1 1 true #document 2 2 true #document 3 3 true #document */F.size 属性
const s = new Set(); s.add(0); s.add(1).add(2).add(2).add(3); console.log(s.size); // 43.Set 构造函数的参数
- 数组
- 字符串、arguments、NodeList、Set 等
1)数组
const s = new Set([1, 2, 1]); console.log(s); // Set(2) { 1, 2 }2)字符串
console.log(new Set('hiii')); // Set(2) { 'h', 'i' }3)arguments
function func() { console.log(new Set(arguments)); } func(1, 2, 1); // Set(2) { 1, 2 }4)NodeList
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <p>1</p> <p>2</p> <p>3</p> <script> console.log(new Set(document.querySelectorAll('P'))); </script> </body> </html>5)Set
const s = new Set([1, 2, 1]); console.log(new Set(s)); // Set(2) { 1, 2 } console.log(s); // Set(2) { 1, 2 } // 这也是复制一个 Set 的方法4.Set 注意事项
A.Set 如何判断重复- Set 对重复值的判断基本遵循严格相等(===)
- 但是对于 NaN 的判断与 === 不同,Set 中 NaN 等于 NaN
const s = new Set(); s.add({}).add({}); console.log({} === {}); // false console.log(s); // Set(2) { {}, {} }B.什么时候使用 Set
- 数组或字符串需要去重时
- 不需要通过下标访问,只需要遍历时
- 为了使用 Set 提供的方法和属性时
5.Set 的应用
1)数组去重const s = new Set([1, 2, 1]); console.log(s); // Set(2) { 1, 2 } console.log([...s]); // [ 1, 2 ]2)字符串去重
const s = new Set('abbacbd'); console.log(s); // Set(4) { 'a', 'b', 'c', 'd' } console.log([...s].join('')); // abcd3)存放 DOM 元素
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <p>1</p> <p>2</p> <p>3</p> <script> // 这里使用 Set 是因为我们不需要通过下标去访问,只需直接遍历即可 const s = new Set(document.querySelectorAll('p')); s.forEach(function (elem) { elem.style.color = 'red'; }); </script> </body> </html>4)遍历
数组的map和filter方法也可以间接用于 Set 了let set = new Set([1, 2, 3]); set = new Set([...set].map(x => x * 2)); // 返回Set结构:{2, 4, 6} let set = new Set([1, 2, 3, 4, 5]); set = new Set([...set].filter(x => (x % 2) == 0)); // 返回Set结构:{2, 4}5)因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)
let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); // 并集 let union = new Set([...a, ...b]); // Set {1, 2, 3, 4} // 交集 let intersect = new Set([...a].filter(x => b.has(x))); // set {2, 3} // (a 相对于 b 的)差集 let difference = new Set([...a].filter(x => !b.has(x))); // Set {1}6.什么是 Map?
什么是 Map?
Map 和 对象 都是键值对的集合// 键 ——> 值,key ——> value // 对象: const person = { name: 'alex', age: 18 }; // Map: const m = new Map(); m.set('name', 'alex'); m.set('age', 18); console.log(m); // Map(2) { 'name' => 'alex', 'age' => 18 } // Map 和 对象 的区别: // 对象一般用字符串当作 “键”(当然在书写时字符串键的引号可以去掉). // Map 中的 “键” 可以是一切类型。 const m = new Map(); m.set(true, 'true'); m.set({}, 'object'); m.set(new Set([1, 2]), 'set'); m.set(undefined, 'undefined'); console.log(m); /* Map(4) { true => 'true', {} => 'object', Set(2) { 1, 2 } => 'set', undefined => 'undefined' } */7.Map 实例的方法和属性
A.set 方法
1)set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键const m = new Map(); m.set('edition', 6) // 键是字符串 m.set(262, 'standard') // 键是数值 m.set(undefined, 'nah') // 键是 undefinedset方法返回的是当前的Map对象,因此可以采用链式写法
let map = new Map() .set(1, 'a') .set(2, 'b') .set(3, 'c');B.get 方法
1)get方法读取key对应的键值,如果找不到key,返回undefinedconst m = new Map(); const hello = function() {console.log('hello');}; m.set(hello, 'Hello ES6!') // 键是函数 m.get(hello) // Hello ES6!C.has 方法
1)has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中const m = new Map(); m.set('edition', 6); m.set(262, 'standard'); m.set(undefined, 'nah'); m.has('edition') // true m.has('years') // false m.has(262) // true m.has(undefined) // trueD.delete 方法
1)delete方法删除某个键,返回true。如果删除失败,返回falseconst m = new Map(); m.set(undefined, 'nah'); m.has(undefined) // true m.delete(undefined) m.has(undefined) // falseE.clear 方法
1)clear方法清除所有成员,没有返回值let map = new Map(); map.set('foo', true); map.set('bar', false); map.size // 2 map.clear() map.size // 0F.forEach 方法
m.forEach(function (value, key, map) { console.log(this); }, document);G.size 属性
1)size属性返回 Map 结构的成员总数const map = new Map(); map.set('foo', true); map.set('bar', false); map.size // 28.Map 构造函数的参数
1)二维数组console.log(new Map([ ['name', 'alex'], ['age', 18] ])); // Map(2) { 'name' => 'alex', 'age' => 18 }2)Set、Map
// Set // Set 中也必须体现出键和值 const s = new Set([ ['name', 'alex'], ['age', 18] ]); console.log(new Map(s)); console.log(s); // Map(2) { 'name' => 'alex', 'age' => 18 } // Set(2) { [ 'name', 'alex' ], [ 'age', 18 ] } // Map const m = new Map([ ['name', 'alex'], ['age', 18] ]); console.log(m); const m2 = new Map(m); console.log(m2, m2 === m); // Map(2) { 'name' => 'alex', 'age' => 18 } // Map(2) { 'name' => 'alex', 'age' => 18 } false // Map 复制的方法9.Map 注意事项
A.Map 如何判断键名是否相同
1)在 Set 中遇到重复的值直接去掉后者,而 Map 中遇到重复的键值则是后面的覆盖前面的- 基本遵循严格相等(===)
- Map 中 NaN 也是等于 NaN
B.什么时候使用 Map
- 如果只是需要键值对结构
- 需要字符串以外的值做键
- 对象一般用在模拟实体上
10.Map 的应用
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <p>1</p> <p>2</p> <p>3</p> <script> const [p1, p2, p3] = document.querySelectorAll('p'); const m = new Map([ [p1, { color: 'red', backgroundColor: 'yellow', fontSize: '40px' }], [p2, { color: 'green', backgroundColor: 'pink', fontSize: '40px' }], [p3, { color: 'blue', backgroundColor: 'orange', fontSize: '40px' }] ]); m.forEach((propObj, elem) => { for (const p in propObj) { elem.style[p] = propObj[p]; } }); // 由于不需要改变 this 指向,所以可以使用箭头函数 </script> </body> </html>B9.
1.同步异步的介绍
A.Promise 是异步操作的一种解决方案
1)异步的概念
异步(Async, async)是与同步(Sync, sync)相对的概念
在我们学习的传统单线程编程中,程序的运行是同步的(同步不意味着所有步骤同时运行,而是指步骤在一个控制流序列中按顺序执行)。而异步的概念则是不保证同步的概念,也就是说,一个异步过程的执行将不再与原有的序列有顺序关系
简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高
以上是关于异步的概念的解释,接下来我们通俗地解释一下异步:异步就是从主线程发射一个子线程来完成任务

B.什么时候用异步编程
在前端编程中(甚至后端有时也是这样),我们在处理一些简短、快速的操作时,例如计算 1 + 1 的结果,往往在主线程中就可以完成。主线程作为一个线程,不能够同时接受多方面的请求。所以,当一个事件没有结束时,界面将无法处理其他请求
现在有一个按钮,如果我们设置它的 onclick 事件为一个死循环,那么当这个按钮按下,整个网页将失去响应
为了避免这种情况的发生,我们常常用子线程来完成一些可能消耗时间足够长以至于被用户察觉的事情(或者是一些需要等待某个时机在背后自动执行的任务,比如:事件监听),比如读取一个大文件或者发出一个网络请求。因为子线程独立于主线程,所以即使出现阻塞也不会影响主线程的运行。但是子线程有一个局限:一旦发射了以后就会与主线程失去同步,我们无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,我们是无法将它合并到主线程中去的
JavaScript 是单线程语言,为了解决多线程问题,JavaScript 中的异步操作函数往往通过回调函数来实现异步任务的结果处理
C.回调函数(callback function)
在 JavaScript 中,回调函数具体的定义为:函数A 作为参数(函数引用)传递到另一个 函数B 中,并且这个 函数B 执行函数A。我们就说 函数A 叫做回调函数。如果没有名称(函数表达式),就叫做匿名回调函数
回调函数就是一个作为参数的函数,它是在我们启动一个异步任务的时候就告诉它:等你完成了这个任务之后要干什么。这样一来主线程几乎不用关心异步任务的状态了,他自己会善始善终
注意:回调和异步不是同一个东西,许多人误认为 js 中每个回调函数都是异步处理的,实际上并不是,可以同步回调,也可以异步回调。只不过说:回调可以是同步也可以是异步,异步必须放在回调里执行,也就是对于一个异步任务只有回调函数里的才是异步的部分
1)回调同步的例子:const test = function (func) { func(); } test(() => { console.log('func'); })2)回调异步的例子:
setTimeout(()=>{ console.log('one'); }, 3000); console.log('two');D.实例
setInterval() 和 是两个异步语句。setTimeout()
1)异步(asynchronous):不会阻塞 CPU 继续执行其他语句,当异步完成时(包含回调函数的主函数的正常语句完成时),会执行 “回调函数”(callback)<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>菜鸟教程(runoob.com)</title> </head> <body> <p>回调函数等待 3 秒后执行。</p> <p id="demo"></p> <p>异步方式,不影响后续执行。</p> <script> function print() { document.getElementById("demo").innerHTML = "RUNOOB!"; } setTimeout(print, 3000); </script> </body> </html>
这段程序中的 setTimeout 就是一个消耗时间较长(3 秒)的过程,它的第一个参数是个回调函数,第二个参数是毫秒数,这个函数执行之后会产生一个子线程,子线程会等待 3 秒,然后执行回调函数 “print”,在命令行输出 “RUNOOB!”
当然,JavaScript 语法十分友好,我们不必单独定义一个函数 print ,我们常常将上面的程序写成:<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>菜鸟教程(runoob.com)</title> </head> <body> <p>回调函数等待 3 秒后执行。</p> <p id="demo"></p> <p>异步方式,不影响后续执行。</p> <script> setTimeout(function () { document.getElementById("demo").innerHTML = "RUNOOB!"; }, 3000); /* ES6 箭头函数写法 setTimeout(() => { document.getElementById("demo").innerHTML = "RUNOOB!"; }, 3000); */ </script> </body> </html>**注意:**既然 setTimeout 会在子线程中等待 3 秒,在 setTimeout 函数执行之后主线程并没有停止,所以:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>菜鸟教程(runoob.com)</title> </head> <body> <p>回调函数等待 3 秒后执行。</p> <p id="demo1"></p> <p id="demo2"></p> <script> setTimeout(function () { document.getElementById("demo1").innerHTML = "RUNOOB-1!"; }, 3000); document.getElementById("demo2").innerHTML = "RUNOOB-2!"; </script> </body> </html>这段程序的执行结果是:

(之前常用的异步操作解决方案是:回调函数)document.addEventListener( 'click', () => { console.log('这里是异步的'); }, false ); console.log('这里是同步的');什么时候使用 Promise 呢?
Promise 一般用来解决层层嵌套的回调函数(回调地狱 callback hell)的问题
例如下面展示两个回调地域的例子:
例子1:分别间隔一秒打印省市县<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>回调举例</title> </head> <body> <script> /* // 此种方式,省市县都会在一秒后同时打印,没有实现要求 setTimeout(() => { console.log("云南省"); }, 1000); setTimeout(() => { console.log("玉溪市"); }, 1000); setTimeout(() => { console.log("峨山县"); }, 1000); */ // 通过回调函数的方式,实现异步 setTimeout(() => { console.log("云南省"); let str01 = "云南省"; setTimeout(() => { console.log(str01 + "玉溪市"); let str02 = "云南省玉溪市"; setTimeout(() => { console.log(str02 + "峨山县"); }, 1000, str02); }, 1000, str01); }, 1000); console.log("通过回调函数的方式,实现异步"); </script> </body> </html>
例子2:当我们点击窗口后,盒子依次 “右——>下——>左” 移动<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"/> <title>Promise</title> <style> * { padding: 0; margin: 0; } #box { width: 300px; height: 300px; background-color: red; transition: all 0.5s; } </style> </head> <body> <div id="box"></div> <script> // 运动函数 const move = (el, {x = 0, y = 0} = {}, end = () => {}) => { el.style.transform = `translate3d(${x}px, ${y}px, 0)`; el.addEventListener( // transitionend 事件在 CSS 完成过渡后触发。 'transitionend', () => { end(); }, false ); }; const boxEl = document.getElementById('box'); // 形成回调地狱 document.addEventListener( 'click', () => { move(boxEl, {x: 150}, () => { move(boxEl, {x: 150, y: 150}, () => { move(boxEl, {y: 150}, () => { move(boxEl, {x: 0, y: 0}); }); }); }); }, false ); </script> </body> </html>
2.Promise 的含义
Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了对象
所谓,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理
Promise 有三个状态:pending(等待)、fulfilled 或 resolved(成功)、rejected(失败)
并且 Promise 必须接收一个回调函数,这个回调函数有两个参数,这两个参数也是两个函数,。(resolve, reject) => {}- 实例化 Promise 后,默认是等待状态
- 当执行 函数时,Promise 从等待状态——>成功状态。resolve()
- 当执行 函数时,Promise 从等待状态——>失败状态。reject()
注意:当 Promise 的状态一但从等待转变为某一个状态后,后续的转变就自动忽略了,比如:先调用 resolve() 再调用 reject(),那么 Promise 的最终结果是成功状态
注意:这里的 resolve reject 只是一个形参,可以取任意名字,但是我们约定直接使用 resolve reject
注意,为了行文方便,本章后面的统一只指状态,不包含状态。resolvedfulfilledrejected
有了对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,对象提供统一的接口,使得控制异步操作更加容易。PromisePromise
Promise也有一些缺点。首先,无法取消,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,内部抛出的错误,不会反应到外部。第三,当处于状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。PromisePromisepending3.Promise 的基本用法
ES6 规定,对象是一个构造函数,用来生成实例下面代码创造了一个实例
const promise = new Promise(function(resolve, reject) { // ... some code if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } });Promise构造函数接受一个函数作为参数,该函数的两个参数分别是和。它们是两个函数,由 JavaScript 引擎提供,不用自己部署
resolve函数的作用是,将对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;函数的作用是,将对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
Promise实例生成以后,可以用方法分别指定状态和状态的回调函数
resolve() 和 函数是可以接收参数的- resolve() 接收的参数会传递给 then 方法的第一个回调函数
- reject() 接收的参数会传递给 then 方法的第二个回调函数
注意:通常我们不仅仅会传递一个基本数据类型的值,我们还常常传递对象,比如再 reject 中传递一个错误对象:
reject(new Error("出错了!"));promise.then(function(value) { // success }, function(error) { // failure });then方法可以接受两个回调函数作为参数。第一个回调函数是对象的状态变为时调用,第二个回调函数是对象的状态变为时调用。这两个函数都是可选的,不一定要提供。它们都接受对象传出的值作为参数
下面是一个对象的简单例子function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, 'done'); }); } timeout(100).then((value) => { console.log(value); });上面代码中,方法返回一个实例,表示一段时间以后才会发生的结果。过了指定的时间(参数)以后,实例的状态变为,就会触发方法绑定的回调函数
Promise 新建后就会立即执行let promise = new Promise(function(resolve, reject) { console.log('Promise'); resolve(); }); promise.then(function() { console.log('resolved.'); }); console.log('Hi!'); // Promise // Hi! // resolved上面代码中,Promise 新建后立即执行,所以首先输出的是。然后,方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以最后输出
下面是异步加载图片的例子function loadImageAsync(url) { return new Promise(function(resolve, reject) { const image = new Image(); image.onload = function() { resolve(image); }; image.onerror = function() { reject(new Error('Could not load image at ' + url)); }; image.src = url; }); }上面代码中,使用包装了一个图片加载的异步操作。如果加载成功,就调用方法,否则就调用方法
如果调用函数和函数时带有参数,那么它们的参数会被传递给回调函数。函数的参数通常是对象的实例,表示抛出的错误;函数的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样const p1 = new Promise(function (resolve, reject) { // ... }); const p2 = new Promise(function (resolve, reject) { // ... resolve(p1); })上面代码中,和都是 Promise 的实例,但是的方法将作为参数,即一个异步操作的结果是返回另一个异步操作
注意,这时的状态就会传递给,也就是说,的状态决定了的状态。如果的状态是,那么的回调函数就会等待的状态改变;如果的状态已经是或者,那么的回调函数将会立刻执行const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error('fail')), 3000) }) const p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000) }) p2 .then(result => console.log(result)) .catch(error => console.log(error)) // Error: fail上面代码中,是一个 Promise,3 秒之后变为。的状态在 1 秒之后改变,方法返回的是。由于返回的是另一个 Promise,导致自己的状态无效了,由的状态决定的状态。所以,后面的语句都变成针对后者()。又过了 2 秒,变为,导致触发方法指定的回调函数
注意,调用或并不会终结 Promise 的参数函数的执行new Promise((resolve, reject) => { resolve(1); console.log(2); }).then(r => { console.log(r); }); // 2 // 1上面代码中,调用以后,后面的还是会执行,并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务
一般来说,调用或以后,Promise 的使命就完成了,后继操作应该放到方法里面,而不应该直接写在或的后面。所以,最好在它们前面加上语句,这样就不会有意外new Promise((resolve, reject) => { return resolve(1); // 后面的语句不会执行 console.log(2); })
2万+

被折叠的 条评论
为什么被折叠?



