Loading... ## 内置对象的扩展 ### 字符串的扩展 #### 模板字符串 **概述:** 模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。 **演示案例:** ```javascript // 普通字符串 let text = `定义一个普通字符串变量` // 多行字符串 let texts = `这是第一行数据 这是第二行数据 这是第三行数据`; // 字符串中嵌入变量 let userName = "Bob", age = 18; console.log(`你好,我叫:${userName},今年${age}岁!`); // 如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。 console.log(`在JavaScript中使用反引号(\`)定义模板字符串`); ``` <div class="tip inlineBlock warning simple"> **⚠️注意:** 模板字符串中嵌入变量,需要将变量名写在 `${}`之中,大括号内部可以放入任意的 JavaScript 表达式,可以进行运算、引用对象属性和调用函数。 模板字符串中的空格和换行,都将被保留。 如果模板字符串中的变量没有声明,将报错而不是输出undefined。 </div> #### 字符串新增的方法 **新增方法列表:** | 方法 | 局部变量 | | -------------------------------------------- | -------------------------------------------- | | includes(searchString:string):boolean | 返回布尔值,表示是否找到了参数字符串 | | startsWith(searchString:string):boolean | 返回布尔值,表示参数字符串是否在原字符串头部 | | endsWith(searchString:string):boolean | 返回布尔值,表示参数字符串是否在原字符串尾部 | | repeat(count:number):string | 返回一个新字符串,表示将原字符串重复n次 | | padStart(length:number,string:string):string | 用于头部补全 | | padEnd(length:number,string:string):string | 用于尾部补全 | | trimStart():string | 消除字符串头部的空格 | | trimEnd():string | 消除尾部的空格 | | replaceAll(searchValue, replacement):string | 替换所有匹配字符串 | | at(index) | 返回参数指定位置的字符 | **演示案例:** ```javascript /** * includes():返回布尔值,表示是否找到了参数字符串。 * startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。 * endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。 */ let s = 'Hello world!'; s.startsWith('Hello') // 是否以'Hello'开头: true s.endsWith('!') // 是否以'!'结尾:true s.includes('o') // 是否包含'o': true // 上面三个方法都支持第二个参数 // 参数2表示开始搜索的位置 s.startsWith('wo', 6) // 从下标6开始计算,是否以'wo'开头:true s.includes('He', 6) // 从下标6开始计算,是否包含'He':false // 参数2表示结束位置 s.endsWith('Hello', 5) // 从第一个字符开始到下标为5的字符中,是否以'Hello'结尾:true /** * repeat(n):返回一个新字符串,表示将原字符串重复n次。 */ 'x'.repeat(3) // "xxx" 'hello'.repeat(2) // "hellohello" 'na'.repeat(0) // "" // 参数如果是小数,会被取整。 'na'.repeat(2.9) // "nana" // 如果repeat的参数是负数或者Infinity(无穷大),会报错。 'na'.repeat(Infinity) // RangeError 'na'.repeat(-1) // RangeError /** * 如果某个字符串不够指定长度,会在头部或尾部补全。 * padStart()用于头部补全 * padEnd()用于尾部补全。 * 第一个参数是字符串补全生效的最大长度,第二个参数是用来补全的字符串。 */ 'x'.padStart(5, 'ab') // 'ababx' 'x'.padStart(4, 'ab') // 'abax' 'x'.padEnd(5, 'ab') // 'xabab' 'x'.padEnd(4, 'ab') // 'xaba' // 如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。 'xxx'.padStart(2, 'ab') // 'xxx' 'xxx'.padEnd(2, 'ab') // 'xxx' // 如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。 'abc'.padStart(10, '0123456789') // '0123456abc' // 如果省略第二个参数,默认使用空格补全长度。 'x'.padStart(4) // ' x' 'x'.padEnd(4) // 'x ' /** * trimStart()和trimEnd()这两个方法。 * 它们的行为与trim()一致。 * trimStart()消除字符串头部的空格 * trimEnd()消除尾部的空格。 * 它们返回的都是新字符串,不会修改原始字符串。 */ const text = ' abc '; text.trim() // "abc" text.trimStart() // "abc " text.trimEnd() // " abc" /** * replaceAll()方法,可以一次性替换所有匹配。 */ let textInfo = 'Hello~~'; textInfo.replace('~', '!'); //Hello!~ textInfo.replaceAll('~', '!'); //Hellpo!! /** * at()方法接受一个整数作为参数,返回参数指定位置的字符,支持负索引(即倒数的位置)。 */ const str = 'hello'; str.at(1) // "e" str.at(-1) // "o" ``` ### 数组的扩展 #### 扩展运算符 **概述:** 扩展运算符(spread)是三个点(`...`)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。 **案例演示:** ```javascript let arr = []; //扩展运算符的基本用法 arr.push(...[1, 2, 3, 4, 5]); console.log(arr); // [1, 2, 3, 4, 5] console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5 //可以放置表达式 console.log(...(1 > 0 ? ['a'] : [])); //a //空数组,不产生效果 console.log([...[], 1]); // [1] //用于函数调用-1 let arrs = []; function push(array, ...items) { array.push(...items); } push(arrs, ...[1, 3, 5, 7, 9]); console.log(arrs); // [1, 3, 5, 7, 9] //用于函数调用-2 function add(x, y) { console.log(x + y); } const numbers = [4, 38]; add(...numbers) // 42 ``` **替代函数的apply方法:** ```javascript // 将一个数组添加到另一个数组的尾部在ES5和ES6中的写法区别 // ES5的 写法 var arr1 = [0, 1, 2]; var arr2 = [3, 4, 5]; Array.prototype.push.apply(arr1, arr2); // ES6 的写法 let arr3 = [0, 1, 2]; let arr4 = [3, 4, 5]; arr3.push(...arr4); ``` ##### 常见应用 *复制数组:*数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。 ```js const a1 = [1, 2]; const a2 = a1; a2[0] = 2; a1 // [2, 2] ``` 上面代码中,`a2`并不是 `a1`的克隆,而是指向同一份数据的另一个指针。修改 `a2`,会直接导致 `a1`的变化。 ES5 只能用变通方法来复制数组。 ```js const a1 = [1, 2]; const a2 = a1.concat(); a2[0] = 2; a1 // [1, 2] ``` 上面代码中,`a1`会返回原数组的克隆,再修改 `a2`就不会对 `a1`产生影响。 扩展运算符提供了复制数组的简便写法。 ```js const a1 = [1, 2]; // 写法一 const a2 = [...a1]; // 写法二 const [...a2] = a1; ``` 上面的两种写法,`a2`都是 `a1`的克隆。 *合并数组:* 扩展运算符提供了数组合并的新写法。 ```js 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' ] ``` ***与解构赋值结合:***扩展运算符可以与解构赋值结合起来,用于生成数组。 ```javascript const [first, ...rest] = [1, 2, 3, 4, 5]; console.log(first); // 1 console.log(rest); // [2, 3, 4, 5] ``` 如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。 ```javascript const [...butLast, last] = [1, 2, 3, 4, 5]; // 报错 const [first, ...middle, last] = [1, 2, 3, 4, 5]; // 报错 ``` *字符串:* 扩展运算符还可以将字符串转为真正的数组。 ```javascript let arr = [...'hello']; console.log(arr); // [ "h", "e", "l", "l", "o" ] ``` ##### 看看它的输出结果是什么? ```javascript let arr1 = [1, 2, 3]; let arr2 = [4, 5, [6, 7, 8]]; arr1.push(...arr2); console.log(arr1); ``` #### Array.from() **概述:**`Array.from()`方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。 下面是一个类似数组的对象,`Array.from()`将它转为真正的数组。【类数组对象,最基本的要求是具有 `length`属性的对象】 ```js let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 }; // ES5 的写法 var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c'] // ES6 的写法 let arr2 = Array.from(arrayLike); // ['a', 'b', 'c'] ``` <div class="tip inlineBlock error simple"> 📢类数组对象转换为真正的数组所需条件: 1. 类数组对象必须具有length属性,用于指定数组的长度 2. 如果没有length属性,那么转换后的数组是一个空数组 3. 类数组对象的属性必须为数值型或字符串型的数字,且从0开始 ```javascript let arrayLike = { 'name': 'tom', 'age': '65', 'sex': '男', 'friends': ['jane', 'john', 'Mary'], length: 4 } let arr = Array.from(arrayLike) console.log(arr) // [undefined, undefined, undefined, undefined] ``` </div> #### includes() **概述:**`Array.prototype.includes`方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的 `includes`方法类似。ES2016 引入了该方法。 ```javascript [1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false [1, 2, NaN].includes(NaN) // true ``` 该方法的第二个参数表示搜索的起始位置,默认为 `0`。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为 `-4`,但数组长度为 `3`),则会重置为从 `0`开始。 ```javascript [1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true ``` #### entries(),keys() 和 values() **概述:** ES6 提供三个新的方法——`entries()`,`keys()`和 `values()`——用于遍历数组。它们都返回一个遍历器对象,可以用 `for...of`循环进行遍历,唯一的区别是 `keys()`是对键名的遍历、`values()`是对键值的遍历,`entries()`是对键值对的遍历。 ```javascript let arr = ['a', 'b']; for (let index of arr.keys()) { console.log(index); } // 0 // 1 for (let elem of arr.values()) { console.log(elem); } // 'a' // 'b' for (let [index, elem] of arr.entries()) { console.log(index, elem); } // 0 "a" // 1 "b" ``` 如果不使用 `for...of`循环,可以手动调用遍历器对象的 `next`方法,进行遍历。【💡扩展】 ```js let letter = ['a', 'b', 'c']; let entries = letter.entries(); console.log(entries.next().value); // [0, 'a'] console.log(entries.next().value); // [1, 'b'] console.log(entries.next().value); // [2, 'c'] ``` #### find(),findIndex(),findLast() 和 findLastIndex() **概念:** 数组实例的 `find()`方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为 `true`的成员,然后返回该成员。如果没有符合条件的成员,则返回 `undefined`。 ```javascript let arr = [1, 4, -5, 10]; // 查找数组中,元素值小于0的数,并返回该值 var res = arr.find((n) => { return n < 0; }); console.log(res); // -5 ``` `find()`方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。 数组实例的 `findIndex()`方法的用法与 `find()`方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回 `-1`。 ```javascript let arr = [1, 5, 10, 6, 20, 30] // 获取数组下标元素大于2的,且数组值大于9的元素下标 let res = arr.findIndex((value, index, arr) => { if (index > 2) { return value > 9; } }) console.log(res); //4 ``` `find()`和 `findIndex()`都是从数组的0号位,依次向后检查。[ES2022](https://github.com/tc39/proposal-array-find-from-last) 新增了两个方法 `findLast()`和 `findLastIndex()`,从数组的最后一个成员开始,依次向前检查,其他都保持不变。 ```javascript let arr = [1, 5, 20, 6, 10, 30] // 从数组结束位置开始查找小于10的数值 let res = arr.findLast((value) => { return value < 10; }) console.log(res); //6 // 从数组结束位置开始查找小于等于10的数值的下标 res = arr.findLastIndex((value) => { return value <= 10; }) console.log(res); //4 ``` ### 对象的扩展 **概述:** 对象(object)是 JavaScript 最重要的数据结构。ES6 对它进行了重大升级。ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。 ```javascript const foo = 'bar'; const baz = {foo}; baz // {foo: "bar"} // 等同于 const baz = {foo: foo}; ``` 上面代码中,变量 `foo`直接写在大括号里面。这时,属性名就是变量名, 属性值就是变量值。 除了属性简写,方法也可以简写。 ```javascript const o = { method() { return "Hello!"; } }; // 等同于 const o = { method: function() { return "Hello!"; } }; ``` **演示:** ```javascript let birth = '2000/01/01'; const Person = { name: '张三', //等同于birth: birth birth, // 等同于hello: function ()... hello() { console.log('我的名字是', this.name); } }; ``` #### 属性名表达式 JavaScript 定义对象的属性,有两种方法: ```javascript // 方法一 obj.foo = true; // 方法二 obj['a' + 'bc'] = 123; ``` 上面代码的方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内。 但是,如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性。 ```javascript var obj = { foo: true, abc: 123 }; ``` ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。 ```javascript let propKey = 'foo'; let obj = { [propKey]: true, ['a' + 'bc']: 123 }; ``` 下面是另一个例子。 ```javascript let lastWord = 'last word'; const a = { 'first word': 'hello', [lastWord]: 'world' }; a['first word'] // "hello" a[lastWord] // "world" a['last word'] // "world" ``` 表达式还可以用于定义方法名。 ```javascript let obj = { ['h' + 'ello']() { return 'hi'; } }; obj.hello() // hi ``` 注意,属性名表达式与简洁表示法,不能同时使用,会报错。 #### Object.is() ES5 比较两个值是否相等,只有两个运算符:相等运算符(`==`)和严格相等运算符(`===`)。它们都有缺点,前者会自动转换数据类型,后者的 `NaN`不等于自身,以及 `+0`等于 `-0`。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。 ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。`Object.is`就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。 ```javascript Object.is('foo', 'foo') // true Object.is({}, {}) // false ``` 不同之处只有两个:一是 `+0`不等于 `-0`,二是 `NaN`等于自身。 ```javascript +0 === -0 //true NaN === NaN // false Object.is(+0, -0) // false Object.is(NaN, NaN) // true ``` #### Object.assign() **概述:** `Object.assign()`方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。 ```javascript const target = { a: 1 }; const source1 = { b: 2 }; const source2 = { c: 3 }; Object.assign(target, source1, source2); target // {a:1, b:2, c:3} ``` `Object.assign()`方法的第一个参数是目标对象,后面的参数都是源对象。 注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。 ```javascript const target = { a: 1, b: 1 }; const source1 = { b: 2, c: 2 }; const source2 = { c: 3 }; Object.assign(target, source1, source2); target // {a:1, b:2, c:3} ``` 如果只有一个参数,`Object.assign()`会直接返回该参数。 ```javascript const obj = {a: 1}; Object.assign(obj) === obj // true ``` 如果该参数不是对象,则会先转成对象,然后返回。 ```javascript typeof Object.assign(2) // "object" ``` 由于 `undefined`和 `null`无法转成对象,所以如果它们作为参数,就会报错。 ```javascript Object.assign(undefined) // 报错 Object.assign(null) // 报错 ``` 如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果 `undefined`和 `null`不在首参数,就不会报错。 ```javascript let obj = {a: 1}; Object.assign(obj, undefined) === obj // true Object.assign(obj, null) === obj // true ``` 其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。 ```javascript const v1 = 'abc'; const v2 = true; const v3 = 10; const obj = Object.assign({}, v1, v2, v3); console.log(obj); // { "0": "a", "1": "b", "2": "c" } ``` 上面代码中,`v1`、`v2`、`v3`分别是字符串、布尔值和数值,结果只有字符串合入目标对象(以字符数组的形式),数值和布尔值都会被忽略。这是因为只有字符串的包装对象,会产生可枚举属性。 ##### 常见用途 *(1)为对象添加属性* ```javascript class Point { constructor(x, y) { Object.assign(this, {x, y}); } } ``` 上面方法通过 `Object.assign()`方法,将 `x`属性和 `y`属性添加到 `Point`类的对象实例。 *(2)为对象添加方法* ```javascript Object.assign(SomeClass.prototype, { someMethod(arg1, arg2) { ··· }, anotherMethod() { ··· } }); // 等同于下面的写法 SomeClass.prototype.someMethod = function (arg1, arg2) { ··· }; SomeClass.prototype.anotherMethod = function () { ··· }; ``` 上面代码使用了对象属性的简洁表示法,直接将两个函数放在大括号中,再使用 `assign()`方法添加到 `SomeClass.prototype`之中。 *(3)克隆对象* ```javascript function clone(origin) { return Object.assign({}, origin); } ``` 上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。 不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。 ```javascript function clone(origin) { let originProto = Object.getPrototypeOf(origin); return Object.assign(Object.create(originProto), origin); } ``` *(4)合并多个对象* 将多个对象合并到某个对象。 ```javascript const merge = (target, ...sources) => Object.assign(target, ...sources); ``` 如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。 ```javascript const merge = (...sources) => Object.assign({}, ...sources); ``` *(5)为属性指定默认值* ```javascript const DEFAULTS = { logLevel: 0, outputFormat: 'html' }; function processContent(options) { options = Object.assign({}, DEFAULTS, options); console.log(options); // ... } ``` 上面代码中,`DEFAULTS`对象是默认值,`options`对象是用户提供的参数。`Object.assign()`方法将 `DEFAULTS`和 `options`合并成一个新对象,如果两者有同名属性,则 `options`的属性值会覆盖 `DEFAULTS`的属性值。 注意,由于存在浅拷贝的问题,`DEFAULTS`对象和 `options`对象的所有属性的值,最好都是简单类型,不要指向另一个对象。否则,`DEFAULTS`对象的该属性很可能不起作用。 ```javascript const DEFAULTS = { url: { host: 'example.com', port: 7070 }, }; processContent({ url: {port: 8000} }) // { // url: {port: 8000} // } ``` 上面代码的原意是将 `url.port`改成 8000,`url.host`不变。实际结果却是 `options.url`覆盖掉 `DEFAULTS.url`,所以 `url.host`就不存在了。 ##### 对象遍历 **Object.keys():** ES5 引入了 `Object.keys`方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。 ```javascript var obj = { foo: 'bar', baz: 42 }; Object.keys(obj) // ["foo", "baz"] ``` ES2017 [引入](https://github.com/tc39/proposal-object-values-entries)了跟 `Object.keys`配套的 `Object.values`和 `Object.entries`,作为遍历一个对象的补充手段,供 `for...of`循环使用。 ```javascript let {keys, values, entries} = Object; let obj = { a: 1, b: 2, c: 3 }; for (let key of keys(obj)) { console.log(key); // 'a', 'b', 'c' } for (let value of values(obj)) { console.log(value); // 1, 2, 3 } for (let [key, value] of entries(obj)) { console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3] } ``` **Object.values():** `Object.values`方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。 ```javascript const obj = { foo: 'bar', baz: 42 }; Object.values(obj) // ["bar", 42] ``` 返回数组的成员顺序,与本章的《属性的遍历》部分介绍的排列规则一致。 ```javascript const obj = { 100: 'a', 2: 'b', 7: 'c' }; Object.values(obj) // ["b", "c", "a"] ``` 上面代码中,属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是 `b`、`c`、`a`。 `Object.values`只返回对象自身的可遍历属性。 ```javascript const obj = Object.create({}, {p: {value: 42}}); Object.values(obj) // [] ``` 上面代码中,`Object.create`方法的第二个参数添加的对象属性(属性 `p`),如果不显式声明,默认是不可遍历的,因为 `p`的属性描述对象的 `enumerable`默认是 `false`,`Object.values`不会返回这个属性。只要把 `enumerable`改成 `true`,`Object.values`就会返回属性 `p`的值。 ```javascript const obj = Object.create({}, {p: { value: 42, enumerable: true } }); Object.values(obj) // [42] ``` `Object.values`会过滤属性名为 Symbol 值的属性。 ```javascript Object.values({ [Symbol()]: 123, foo: 'abc' }); // ['abc'] ``` 如果 `Object.values`方法的参数是一个字符串,会返回各个字符组成的一个数组。 ```javascript Object.values('foo') // ['f', 'o', 'o'] ``` 上面代码中,字符串会先转成一个类似数组的对象。字符串的每个字符,就是该对象的一个属性。因此,`Object.values`返回每个属性的键值,就是各个字符组成的一个数组。 如果参数不是对象,`Object.values`会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,`Object.values`会返回空数组。 ```javascript Object.values(42) // [] Object.values(true) // [] ``` **Object.entries():** `Object.entries()`方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。 ```javascript const obj = { foo: 'bar', baz: 42 }; Object.entries(obj) // [ ["foo", "bar"], ["baz", 42] ] ``` 除了返回值不一样,该方法的行为与 `Object.values`基本一致。 如果原对象的属性名是一个 Symbol 值,该属性会被忽略。 ```javascript Object.entries({ [Symbol()]: 123, foo: 'abc' }); // [ [ 'foo', 'abc' ] ] ``` 上面代码中,原对象有两个属性,`Object.entries`只输出属性名非 Symbol 值的属性。将来可能会有 `Reflect.ownEntries()`方法,返回对象自身的所有属性。 `Object.entries`的基本用途是遍历对象的属性。 ```javascript let obj = { one: 1, two: 2 }; for (let [k, v] of Object.entries(obj)) { console.log( `${JSON.stringify(k)}: ${JSON.stringify(v)}` ); } // "one": 1 // "two": 2 ``` `Object.entries`方法的另一个用处是,将对象转为真正的 `Map`结构。 ```javascript const obj = { foo: 'bar', baz: 42 }; const map = new Map(Object.entries(obj)); map // Map { foo: "bar", baz: 42 } ``` 自己实现 `Object.entries`方法,非常简单。 ```javascript // Generator函数的版本 function* entries(obj) { for (let key of Object.keys(obj)) { yield [key, obj[key]]; } } // 非Generator函数的版本 function entries(obj) { let arr = []; for (let key of Object.keys(obj)) { arr.push([key, obj[key]]); } return arr; } ``` ## Set和Map对象 ### Set对象 **概述:** ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。 `Set`本身是一个构造函数,用来生成 Set 数据结构。 ```javascript const s = new Set(); [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x)); for (let i of s) { console.log(i); } // 2 3 5 4 ``` 上面代码通过 `add()`方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。 **常见属性和方法:** | 属性/方法 | 描述 | | --------------------------- | -------------------------------------------- | | Set.prototype.constructor | 构造函数,默认就是Set函数 | | Set.prototype.size | 返回Set实例的成员总数 | | Set.prototype.add(value) | 添加某个值,返回 Set 结构本身 | | Set.prototype.delete(value) | 删除某个值,返回一个布尔值,表示删除是否成功 | | Set.prototype.has(value) | 返回一个布尔值,表示该值是否为Set的成员 | | Set.prototype.clear() | 清除所有成员,没有返回值 | | Set.prototype.keys() | 返回键名的遍历器 | | Set.prototype.values() | 返回键值的遍历器 | | Set.prototype.entries() | 返回键值对的遍历器 | | Set.prototype.forEach() | 使用回调函数遍历每个成员 | ### Map对象 **概述:** JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。 为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。 **演示案例:** ```javascript const m = new Map(); m.set("1001", { name: "张三", age: 16 }); m.set("1002", { name: "李四", age: 10 }); console.log(m.size); // 2 console.log(m.has('1000')); // false console.log(m.get('1001')); // {name: '张三', age: 16} console.log(m.delete('1001')); //true console.log(m.delete('1001')); //false console.log(m.size); //1 //Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。 const map = new Map([ ['name', '张三'], ['title', 'Author'] ]); for (const iterator of map.keys()) { console.log(iterator); //name title } ``` 如果对同一个键多次赋值,后面的值将覆盖前面的值。 ```javascript const map = new Map(); map .set(1, 'aaa') .set(1, 'bbb'); map.get(1) // "bbb" ``` 上面代码对键 `1`连续赋值两次,后一次的值覆盖前一次的值。 数组的 `map`和 `filter`方法: ```javascript let arr = [2, 4, 6, 8]; let res = arr.map((v) => { return v * 2; }) console.log(res); // [4, 8, 12, 16] let arr2 = [1, 2, 3, 4, 5]; let res2 = arr2.filter((v) => { return v % 2 == 0; }); console.log(res2); // [2, 4] ``` **常见属性和方法:** | 属性/方法 | 描述 | | ------------------------------ | -------------------------------------------------- | | size 属性 | 返回 Map 结构的成员总数 | | Map.prototype.set(key, value) | 设置键名key对应的键值为value,返回整个 Map 结构 | | Map.prototype.get(key) | 读取key对应的键值,如果找不到key,返回undefined | | Map.prototype.has(key) | 返回一个布尔值,表示某个键是否在当前 Map 对象之中 | | Map.prototype.delete(key) | 删除某个键,返回true。如果删除失败,返回false | | Map.prototype.clear() | 清除所有成员,没有返回值 | | Map.prototype.keys() | 返回键名的遍历器 | | Map.prototype.values() | 返回键值的遍历器 | | Map.prototype.entries() | 返回所有成员的遍历器 | | Map.prototype.forEach() | 遍历 Map 的所有成员 | ## Module语法 **概述:** 历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。 ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。ES6模块带来的好处:避免变量污染,命名冲突、提高代码复用率、提高维护性、依赖关系的管理。 模块功能主要由两个命令构成: 1. `export`命令:用于规定模块的对外接口 2. `import`命令:用于输入其他模块提供的功能 ### export和import命令 **export(导出):** 一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用 `export`关键字输出该变量。下面是一个 JS 文件,里面使用 `export`命令输出变量。 ```javascript // profile.js export var firstName = 'Michael'; export var lastName = 'Jackson'; export var year = 1958; ``` 上面代码是 `profile.js`文件,保存了用户信息。ES6 将其视为一个模块,里面用 `export`命令对外部输出了三个变量。 `export`的写法,除了像上面这样,还有另外一种。 ```javascript // profile.js var firstName = 'Michael'; var lastName = 'Jackson'; var year = 1958; export { firstName, lastName, year }; ``` 上面代码在 `export`命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在 `var`语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。 `export`命令除了输出变量,还可以输出函数或类(class)。 ```javascript export function multiply(x, y) { return x * y; }; ``` 上面代码对外输出一个函数 `multiply`。 通常情况下,`export`输出的变量就是本来的名字,但是可以使用 `as`关键字重命名。 ```javascript function v1() { ... } function v2() { ... } export { v1 as streamV1, v2 as streamV2, v2 as streamLatestVersion }; ``` 上面代码使用 `as`关键字,重命名了函数 `v1`和 `v2`的对外接口。重命名后,`v2`可以用不同的名字输出两次。 需要特别注意的是,`export`命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。 ```javascript // 报错 export 1; // 报错 var m = 1; export m; ``` 上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量 `m`,还是直接输出 1。`1`只是一个值,不是接口。正确的写法是下面这样。 ```javascript // 写法一 export var m = 1; // 写法二 var m = 1; export {m}; // 写法三 var n = 1; export {n as m}; ``` 上面三种写法都是正确的,规定了对外的接口 `m`。其他脚本可以通过这个接口,取到值 `1`。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。 同样的,`function`和 `class`的输出,也必须遵守这样的写法。 ```javascript // 报错 function f() {} export f; // 正确 export function f() {}; // 正确 function f() {} export {f}; ``` **import(导入):** 使用 `export`命令定义了模块的对外接口以后,其他 JS 文件就可以通过 `import`命令加载这个模块。 ```javascript // main.js import { firstName, lastName, year } from './profile.js'; function setName(element) { element.textContent = firstName + ' ' + lastName; } ``` 上面代码的 `import`命令,用于加载 `profile.js`文件,并从中输入变量。`import`命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(`profile.js`)对外接口的名称相同。 如果想为输入的变量重新取一个名字,`import`命令要使用 `as`关键字,将输入的变量重命名。 ```javascript import { lastName as surname } from './profile.js'; ``` `import`命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。 ```javascript import {a} from './xxx.js' a = {}; // Syntax Error : 'a' is read-only; ``` 上面代码中,脚本加载了变量 `a`,对其重新赋值就会报错,因为 `a`是一个只读的接口。但是,如果 `a`是一个对象,改写 `a`的属性是允许的。 ```javascript import {a} from './xxx.js' a.foo = 'hello'; // 合法操作 ``` 上面代码中,`a`的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,不要轻易改变它的属性。 `import`后面的 `from`指定模块文件的位置,可以是相对路径,也可以是绝对路径。如果不带有路径,只是一个模块名,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。 ```javascript import { myMethod } from 'util'; ``` 上面代码中,`util`是模块文件名,由于不带有路径,必须通过配置,告诉引擎怎么取到这个模块。 注意,`import`命令具有提升效果,会提升到整个模块的头部,首先执行。 ```javascript foo(); import { foo } from 'my_module'; ``` 上面的代码不会报错,因为 `import`的执行早于 `foo`的调用。这种行为的本质是,`import`命令是编译阶段执行的,在代码运行之前。 由于 `import`是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。 ```javascript // 报错 import { 'f' + 'oo' } from 'my_module'; // 报错 let module = 'my_module'; import { foo } from module; // 报错 if (x === 1) { import { foo } from 'module1'; } else { import { foo } from 'module2'; } ``` 上面三种写法都会报错,因为它们用到了表达式、变量和 `if`结构。在静态分析阶段,这些语法都是没法得到值的。 最后,`import`语句会执行所加载的模块,因此可以有下面的写法。 ```javascript import 'lodash'; ``` 上面代码仅仅执行 `lodash`模块,但是不输入任何值。 如果多次重复执行同一句 `import`语句,那么只会执行一次,而不会执行多次。 ```javascript import 'lodash'; import 'lodash'; ``` 上面代码加载了两次 `lodash`,但是只会执行一次。 ```javascript import { foo } from 'my_module'; import { bar } from 'my_module'; // 等同于 import { foo, bar } from 'my_module'; ``` 上面代码中,虽然 `foo`和 `bar`在两个语句中加载,但是它们对应的是同一个 `my_module`模块。也就是说,`import`语句是 Singleton 模式。 **模块整体加载:** 除了指定加载某个输出值,还可以使用整体加载,即用星号(`*`)指定一个对象,所有输出值都加载在这个对象上面。 下面是一个 `circle.js`文件,它输出两个方法 `area`和 `circumference`。 ```javascript // circle.js export function area(radius) { return Math.PI * radius * radius; } export function circumference(radius) { return 2 * Math.PI * radius; } ``` 现在,加载这个模块。 ```javascript // main.js import { area, circumference } from './circle'; console.log('圆面积:' + area(4)); console.log('圆周长:' + circumference(14)); ``` 上面写法是逐一指定要加载的方法,整体加载的写法如下。 ```javascript import * as circle from './circle'; console.log('圆面积:' + circle.area(4)); console.log('圆周长:' + circle.circumference(14)); ``` 注意,模块整体加载所在的那个对象(上例是 `circle`),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。 ```javascript import * as circle from './circle'; // 下面两行都是不允许的 circle.foo = 'hello'; circle.area = function () {}; ``` ### export default命令 **概述:** 使用 `import`命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。 为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到 `export default`命令,为模块指定默认输出。 ```javascript // export-default.js export default function () { console.log('foo'); } ``` 上面代码是一个模块文件 `export-default.js`,它的默认输出是一个函数。 其他模块加载该模块时,`import`命令可以为该匿名函数指定任意名字。 ```javascript // import-default.js import customName from './export-default'; customName(); // 'foo' ``` 上面代码的 `import`命令,可以用任意名称指向 `export-default.js`输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时 `import`命令后面,不使用大括号。 `export default`命令用在非匿名函数前,也是可以的。 ```javascript // export-default.js export default function foo() { console.log('foo'); } // 或者写成 function foo() { console.log('foo'); } export default foo; ``` 上面代码中,`foo`函数的函数名 `foo`,在模块外部是无效的。加载的时候,视同匿名函数加载。 下面比较一下默认输出和正常输出。 ```javascript // 第一组 export default function crc32() { // 输出 // ... } import crc32 from 'crc32'; // 输入 // 第二组 export function crc32() { // 输出 // ... }; import {crc32} from 'crc32'; // 输入 ``` 上面代码的两组写法,第一组是使用 `export default`时,对应的 `import`语句不需要使用大括号;第二组是不使用 `export default`时,对应的 `import`语句需要使用大括号。 `export default`命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此 `export default`命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应 `export default`命令。 本质上,`export default`就是输出一个叫做 `default`的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。 ```javascript // modules.js function add(x, y) { return x * y; } export {add as default}; // 等同于 // export default add; // app.js import { default as foo } from 'modules'; // 等同于 // import foo from 'modules'; ``` 正是因为 `export default`命令其实只是输出一个叫做 `default`的变量,所以它后面不能跟变量声明语句。 ```javascript // 正确 export var a = 1; // 正确 var a = 1; export default a; // 错误 export default var a = 1; ``` 上面代码中,`export default a`的含义是将变量 `a`的值赋给变量 `default`。所以,最后一种写法会报错。 同样地,因为 `export default`命令的本质是将后面的值,赋给 `default`变量,所以可以直接将一个值写在 `export default`之后。 ```javascript // 正确 export default 42; // 报错 export 42; ``` 上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定对外接口为 `default`。 有了 `export default`命令,输入模块时就非常直观了,以输入 lodash 模块为例。 ```javascript import _ from 'lodash'; ``` 如果想在一条 `import`语句中,同时输入默认方法和其他接口,可以写成下面这样。 ```javascript import _, { each, forEach } from 'lodash'; ``` 对应上面代码的 `export`语句如下。 ```javascript export default function (obj) { // ··· } export function each(obj, iterator, context) { // ··· } export { each as forEach }; ``` 上面代码的最后一行的意思是,暴露出 `forEach`接口,默认指向 `each`接口,即 `forEach`和 `each`指向同一个方法。 `export default`也可以用来输出类。 ```javascript // MyClass.js export default class { ... } // main.js import MyClass from 'MyClass'; let o = new MyClass(); ``` ## Promise 对象 【扩展】 **概述:** Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 `Promise`对象。 所谓 `Promise`,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。 ES6 规定,`Promise`对象是一个构造函数,用来生成 `Promise`实例。 下面代码创造了一个 `Promise`实例。 ```javascript const promise = new Promise(function(resolve, reject) { // 这里执行对应的异步操作代码 if (/* 异步操作成功 */){ resolve(value); } else { reject(error); } }); ``` `Promise`构造函数接受一个函数作为参数,该函数的两个参数分别是 `resolve`和 `reject`。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。 `resolve`函数的作用是,将 `Promise`对象的状态从“**未完成**”变为“**成功**”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;`reject`函数的作用是,将 `Promise`对象的状态从“**未完成**”变为“**失败**”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。 `Promise`实例生成以后,可以用 `then`方法分别指定 `resolved`状态和 `rejected`状态的回调函数。 ```javascript promise.then(function(value) { // success }, function(error) { // failure }); ``` `then`方法可以接受两个回调函数作为参数。第一个回调函数是 `Promise`对象的状态变为 `resolved`时调用,第二个回调函数是 `Promise`对象的状态变为 `rejected`时调用。这两个函数都是可选的,不一定要提供。它们都接受 `Promise`对象传出的值作为参数。 下面是一个 `Promise`对象的简单例子。 ```javascript function timeout(ms) { return new Promise((resolve, reject) => { //模拟异步请求耗时 setTimeout(() => { //产生一个随机数字 let num = Math.floor(Math.random() * 10 + 1); //如果产生的随机数是偶数,则表示响应成功 if (num % 2 == 0) { resolve(num); //正常返回数据 } //否则响应失败 reject(`请求失败,错误编号:${num}`); //返回包装数据 }, ms); }); } //请求方法 timeout(100).then((value) => { console.log(value); }); ``` 上面代码中,`timeout`方法返回一个 `Promise`实例,表示一段时间以后才会发生的结果。过了指定的时间(`ms`参数)以后,`Promise`实例的状态变为 `resolved`或 `reject`,就会触发 `then`方法绑定的回调函数。 Promise 新建后就会立即执行。 ```javascript let promise = new Promise(function(resolve, reject) { console.log('Promise'); resolve(); }); promise.then(function() { console.log('resolved.'); }); console.log('Hi!'); // Promise // Hi! // resolved ``` 上面代码中,Promise 新建后立即执行,所以首先输出的是 `Promise`。然后,`then`方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以 `resolved`最后输出。 下面是异步加载图片的例子。 ```javascript function loadImageAsync(url) { return new Promise(function (resolve, reject) { const image = new Image(); image.src = url; //指定图片土建 //当图片成功加载后,则返回图片数据 image.onload = function () { resolve(image); }; //当图片加载失败时,则引发异常 image.onerror = function () { throw '图片加载失败'; }; //设置一个定时器,模拟图片超时加载,200毫秒后, setTimeout(() => { reject('图片加载超时') }, 500); }) } //调用方法 loadImageAsync('https://www.baidu1.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png') .then((res) => { console.log(res); }, (err) => { console.log(err); }).catch((err) => { console.log('错误:' + err); }).finally(() => { console.log('方法执行结束'); }) ``` 上面代码中,使用 `Promise`包装了一个图片加载的异步操作。如果加载成功,就调用 `resolve`方法,否则就调用 `reject`方法。 更多的内容请,请访问:[https://es6.ruanyifeng.com/#docs/promise](https://es6.ruanyifeng.com/#docs/promise) --- <div class="tip inlineBlock info simple"> ▶️ 开始学习下一章: [JavaScript+Jquery+ES6入门到放弃之jQuery基础](https://jbea.cn/archives/568.html) </div> --- 最后修改:2022 年 09 月 06 日 © 允许规范转载 赞 2 都滑到这里了,不点赞再走!?