-
变量的解构赋值
- 默认值生效的条件是,对象的属性值严格等于(
===)undefined。
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null
//-----
function move({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
move({x: 3, y: 8}); // [3, 8]
move({x: 3}); // [3, undefined]
move({}); // [undefined, undefined]
move(); // [0, 0]
//-----
[1, undefined, 3].map((x = 'yes') => x);
// [ 1, 'yes', 3 ]
复制代码
-
如果要将一个已经声明的变量用于解构赋值,必须非常小心。下面代码的写法会报错,因为 JavaScript 引擎会将
{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。 -
下面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error
// 正确的写法
let x;
({x} = {x: 1});
复制代码
- 解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于
undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。
let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
复制代码
-
圆括号问题
解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。
由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。
但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。
-
不能使用圆括号的情况
-
变量声明语句(因为它们都是变量声明语句,模式不能使用圆括号。)
// 全部报错 let [(a)] = [1]; let {x: (c)} = {}; let ({x: c}) = {}; let {(x: c)} = {}; let {(x): c} = {}; let { o: ({ p: p }) } = { o: { p: 2 } }; 复制代码 -
函数参数(函数参数也属于变量声明,因此不能带有圆括号。)
// 报错 function f([(z)]) { return z; } // 报错 function f([z,(x)]) { return x; } 复制代码 -
赋值语句的模式(代码将整个模式放在圆括号之中,导致报错。)
// 全部报错 ({ p: a }) = { p: 42 }; ([a]) = [5]; 复制代码// 报错 代码将一部分模式放在圆括号之中,导致报错。 [({ p: a }), { x: c }] = [{}, {}]; 复制代码
-
-
可以使用圆括号的情况
-
可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是
p,而不是d;第三行语句与第一行语句的性质一致。[(b)] = [3]; // 正确 ({ p: (d) } = {}); // 正确 [(parseInt.prop)] = [3]; // 正确 复制代码
-
-
正则的扩展
-
实际上,
y修饰符号隐含了头部匹配的标志^; -
ES6 还为正则表达式添加了
y修饰符,叫做“粘连”(sticky)修饰符。y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。
-
具名组匹配:
const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/; const matchObj = RE_DATE.exec('1999-12-31'); const year = matchObj.groups.year; // "1999" const month = matchObj.groups.month; // "12" const day = matchObj.groups.day; // "31" 复制代码 -
解构赋值和替换:字符串替换时,使用
$<组名>引用具名组。let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u; '2015-01-02'.replace(re, '$<day>/$<month>/$<year>') // '02/01/2015' 复制代码 -
上面代码中,
replace方法的第二个参数是一个字符串,而不是正则表达式。replace方法的第二个参数也可以是函数,该函数的参数序列如下。'2015-01-02'.replace(re, ( matched, // 整个匹配结果 2015-01-02 capture1, // 第一个组匹配 2015 capture2, // 第二个组匹配 01 capture3, // 第三个组匹配 02 position, // 匹配开始的位置 0 S, // 原字符串 2015-01-02 groups // 具名组构成的一个对象 {year, month, day} ) => { let {day, month, year} = groups; return `${day}/${month}/${year}`; }); 复制代码具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。
-
引用:
如果要在正则表达式内部引用某个“具名组匹配”,可以使用
\k<组名>的写法。const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/; RE_TWICE.test('abc!abc') // true RE_TWICE.test('abc!ab') // false 复制代码数字引用(
\1)依然有效。const RE_TWICE = /^(?<word>[a-z]+)!\1$/; RE_TWICE.test('abc!abc') // true RE_TWICE.test('abc!ab') // false 复制代码这两种引用语法还可以同时使用。
const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/; RE_TWICE.test('abc!abc!abc') // true RE_TWICE.test('abc!abc!ab') // false 复制代码
-
格式化金额数字
function formatNumberToMoney(money) { const reg = /(?<=\d)(\d{3})/g; // 为了便于将大数也格式化BigInit,先将末位的n去掉 return (`${money}` || "").replace("n", "").replaceAll(reg, ",$1"); } function formatNumberToMoney(money) { return Number(money).toLocalString(); } 复制代码
-
-
函数
- 使用参数默认值时,函数不能有同名参数。
// 不报错 function foo(x, x, y) { // ... } // 报错 function foo(x, x, y = 1) { // ... } // SyntaxError: Duplicate parameter name not allowed in this context 复制代码另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。
let x = 99; function foo(p = x + 1) { console.log(p); } foo() // 100 x = 100; foo() // 101 复制代码上面代码中,参数
p的默认值是x + 1。这时,每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100。
-
函数的
length属性指定了默认值以后,函数的
length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。(function (a) {}).length // 1 (function (a = 5) {}).length // 0 (function (a, b, c = 5) {}).length // 2 复制代码上面代码中,
length属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了 3 个参数,其中有一个参数c指定了默认值,因此length属性等于3减去1,最后得到2。这是因为
length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入length属性。(function(...args) {}).length // 0 复制代码如果设置了默认值的参数不是尾参数,那么
length属性也不再计入后面的参数了。(function (a = 0, b, c) {}).length // 0 (function (a, b = 1, c) {}).length // 1 复制代码
-
作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
var x = 1; function f(x, y = x) { console.log(y); } f(2) // 2 复制代码上面代码中,参数
y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2。再看下面的例子。
let x = 1; function f(y = x) { let x = 2; console.log(y); } f() // 1 复制代码上面代码中,函数
f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x。如果此时,全局变量
x不存在,就会报错。function f(y = x) { let x = 2; console.log(y); } f() // ReferenceError: x is not defined 复制代码下面这样写,也会报错。
var x = 1; function foo(x = x) { // ... } foo() // ReferenceError: x is not defined 复制代码上面代码中,参数
x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“。如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。
let foo = 'outer'; function bar(func = () => foo) { let foo = 'inner'; console.log(func()); } bar(); // outer 复制代码上面代码中,函数
bar的参数func的默认值是一个匿名函数,返回值为变量foo。函数参数形成的单独作用域里面,并没有定义变量foo,所以foo指向外层的全局变量foo,因此输出outer。如果写成下面这样,就会报错。
function bar(func = () => foo) { let foo = 'inner'; console.log(func()); } bar() // ReferenceError: foo is not defined 复制代码上面代码中,匿名函数里面的
foo指向函数外层,但是函数外层并没有声明变量foo,所以就报错了。下面是一个更复杂的例子。
var x = 1; function foo(x, y = function() { x = 2; }) { var x = 3; y(); console.log(x); } foo() // 3 x // 1 复制代码上面代码中,函数
foo的参数形成一个单独作用域。这个作用域里面,首先声明了变量x,然后声明了变量y,y的默认值是一个匿名函数。这个匿名函数内部的变量x,指向同一个作用域的第一个参数x。函数foo内部又声明了一个内部变量x,该变量与第一个参数x由于不是同一个作用域,所以不是同一个变量,因此执行y后,内部变量x和外部全局变量x的值都没变。如果将
var x = 3的var去除,函数foo的内部变量x就指向第一个参数x,与匿名函数内部的x是一致的,所以最后输出的就是2,而外层的全局变量x依然不受影响。var x = 1; function foo(x, y = function() { x = 2; }) { x = 3; y(); console.log(x); } foo() // 2 x // 1 复制代码
-
函数的扩展
-
严格模式:
从 ES5 开始,函数内部可以设定为严格模式。
function doSomething(a, b) { 'use strict'; // code } 复制代码ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。
// 报错 function doSomething(a, b = a) { 'use strict'; // code } // 报错 const doSomething = function ({a, b}) { 'use strict'; // code }; // 报错 const doSomething = (...a) => { 'use strict'; // code }; const obj = { // 报错 doSomething({a, b}) { 'use strict'; // code } }; 复制代码这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。
// 报错 function doSomething(value = 070) { 'use strict'; return value; } 复制代码上面代码中,参数
value的默认值是八进制数070,但是严格模式下不能用前缀0表示八进制,所以应该报错。但是实际上,JavaScript 引擎会先成功执行value = 070,然后进入函数体内部,发现需要用严格模式执行,这时才会报错。虽然可以先解析函数体代码,再执行参数代码,但是这样无疑就增加了复杂性。因此,标准索性禁止了这种用法,只要参数使用了默认值、解构赋值、或者扩展运算符,就不能显式指定严格模式。
两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的。
'use strict'; function doSomething(a, b = a) { // code } 复制代码第二种是把函数包在一个无参数的立即执行函数里面。
const doSomething = (function () { 'use strict'; return function(value = 42) { return value; }; }()); 复制代码
-
箭头函数
箭头函数有几个使用注意点。
(1)箭头函数没有自己的
this对象(详见下文)。(2)不可以当作构造函数,也就是说,不可以对箭头函数使用
new命令,否则会抛出一个错误。(3)不可以使用
arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。(4)不可以使用
yield命令,因此箭头函数不能用作 Generator 函数。上面四点中,最重要的是第一点。对于普通函数来说,内部的
this指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的this对象,内部的this就是定义时上层作用域中的this。也就是说,箭头函数内部的this指向是固定的,相比之下,普通函数的this指向是可变的。function foo() { setTimeout(() => { console.log('id:', this.id); }, 100); } var id = 21; foo.call({ id: 42 }); // id: 42 复制代码上面代码中,
setTimeout()的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以打印出来的是42。下面例子是回调函数分别为箭头函数和普通函数,对比它们内部的
this指向。function Timer() { this.s1 = 0; this.s2 = 0; // 箭头函数 setInterval(() => this.s1++, 1000); // 普通函数 setInterval(function () { this.s2++; }, 1000); } var timer = new Timer(); setTimeout(() => console.log('s1: ', timer.s1), 3100); setTimeout(() => console.log('s2: ', timer.s2), 3100); // s1: 3 // s2: 0 复制代码上面代码中,
Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。箭头函数实际上可以让
this指向固定化,绑定this使得它不再可变,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。var handler = { id: '123456', init: function() { document.addEventListener('click', event => this.doSomething(event.type), false); }, doSomething: function(type) { console.log('Handling ' + type + ' for ' + this.id); } }; 复制代码上面代码的
init()方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象。如果回调函数是普通函数,那么运行this.doSomething()这一行会报错,因为此时this指向document对象。总之,箭头函数根本没有自己的
this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。下面是 Babel 转箭头函数产生的 ES5 代码,就能清楚地说明
this的指向。// ES6 function foo() { setTimeout(() => { console.log('id:', this.id); }, 100); } // ES5 function foo() { var _this = this; setTimeout(function () { console.log('id:', _this.id); }, 100); } 复制代码上面代码中,转换后的 ES5 版本清楚地说明了,箭头函数里面根本没有自己的
this,而是引用外层的this。请问下面的代码之中,
this的指向有几个?function foo() { return () => { return () => { return () => { console.log('id:', this.id); }; }; }; } var f = foo.call({id: 1}); var t1 = f.call({id: 2})()(); // id: 1 var t2 = f().call({id: 3})(); // id: 1 var t3 = f()().call({id: 4}); // id: 1 复制代码答案是
this的指向只有一个,就是函数foo的this,这是因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this。所以不管怎么嵌套,t1、t2、t3都输出同样的结果。如果这个例子的所有内层函数都写成普通函数,那么每个函数的this都指向运行时所在的不同对象。除了
this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。function foo() { setTimeout(() => { console.log('args:', arguments); }, 100); } foo(2, 4, 6, 8) // args: [2, 4, 6, 8] 复制代码上面代码中,箭头函数内部的变量
arguments,其实是函数foo的arguments变量。另外,由于箭头函数没有自己的
this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。(function() { return [ (() => this.x).bind({ x: 'inner' })() ]; }).call({ x: 'outer' }); // ['outer'] 复制代码上面代码中,箭头函数没有自己的
this,所以bind方法无效,内部的this指向外部的this。长期以来,JavaScript 语言的
this对象一直是一个令人头痛的问题,在对象方法中使用this,必须非常小心。箭头函数”绑定”this,很大程度上解决了这个困扰。
-
不适合使用箭头函数的场合
由于箭头函数使得
this从“动态”变成“静态”,下面两个场合不应该使用箭头函数。第一个场合是定义对象的方法,且该方法内部包括
this。const cat = { lives: 9, jumps: () => { this.lives--; } } 复制代码上面代码中,
cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps箭头函数定义时的作用域就是全局作用域。再看一个例子。
globalThis.s = 21; const obj = { s: 42, m: () => console.log(this.s) }; obj.m() // 21 复制代码上面例子中,
obj.m()使用箭头函数定义。JavaScript 引擎的处理方法是,先在全局空间生成这个箭头函数,然后赋值给obj.m,这导致箭头函数内部的this指向全局对象,所以obj.m()输出的是全局空间的21,而不是对象内部的42。上面的代码实际上等同于下面的代码。globalThis.s = 21; globalThis.m = () => console.log(this.s); const obj = { s: 42, m: globalThis.m }; obj.m() // 21 复制代码由于上面这个原因,对象的属性建议使用传统的写法定义,不要用箭头函数定义。
第二个场合是需要动态
this的时候,也不应使用箭头函数。var button = document.getElementById('press'); button.addEventListener('click', () => { this.classList.toggle('on'); }); 复制代码上面代码运行时,点击按钮会报错,因为
button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。另外,如果函数体很复杂,有许多行,或者函数内部有大量的读写操作,不单纯是为了计算值,这时也不应该使用箭头函数,而是要使用普通函数,这样可以提高代码可读性。
-
尾调用优化
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数
A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
function f() { let m = 1; let n = 2; return g(m + n); } f(); // 等同于 function f() { return g(3); } f(); // 等同于 g(3); 复制代码上面代码中,如果函数
g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f(x)的调用帧,只保留g(3)的调用帧。这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
function addOne(a){ var one = 1; function inner(b){ return b + one; } return inner(a); } 复制代码上面的函数不会进行尾调用优化,因为内层函数
inner用到了外层函数addOne的内部变量one。注意,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持。
-
尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } factorial(5) // 120 复制代码上面代码是一个阶乘函数,计算
n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
function factorial(n, total) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120 复制代码还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性。
非尾递归的 Fibonacci 数列实现如下。
function Fibonacci (n) { if ( n <= 1 ) {return 1}; return Fibonacci(n - 1) + Fibonacci(n - 2); } Fibonacci(10) // 89 Fibonacci(100) // 超时 Fibonacci(500) // 超时 复制代码尾递归优化过的 Fibonacci 数列实现如下。
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) { if( n <= 1 ) {return ac2}; return Fibonacci2 (n - 1, ac2, ac1 + ac2); } Fibonacci2(100) // 573147844013817200000 Fibonacci2(1000) // 7.0330367711422765e+208 Fibonacci2(10000) // Infinity 复制代码由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 亦是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出(或者层层递归造成的超时),相对节省内存。
-
-
数组的扩展
-
数组的空位
数组的空位指,数组的某一个位置没有任何值。比如,
Array构造函数返回的数组都是空位。Array(3) // [, , ,] 复制代码上面代码中,
Array(3)返回一个具有 3 个空位的数组。注意,空位不是
undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。0 in [undefined, undefined, undefined] // true 0 in [, , ,] // false 复制代码上面代码说明,第一个数组的 0 号位置是有值的,第二个数组的 0 号位置没有值。
ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。
forEach(),filter(),reduce(),every()和some()都会跳过空位。map()会跳过空位,但会保留这个值join()和toString()会将空位视为undefined,而undefined和null会被处理成空字符串。
// forEach方法 [,'a'].forEach((x,i) => console.log(i)); // 1 // filter方法 ['a',,'b'].filter(x => true) // ['a','b'] // every方法 [,'a'].every(x => x==='a') // true // reduce方法 [1,,2].reduce((x,y) => x+y) // 3 // some方法 [,'a'].some(x => x !== 'a') // false // map方法 [,'a'].map(x => 1) // [,1] // join方法 [,'a',undefined,null].join('#') // "#a##" // toString方法 [,'a',undefined,null].toString() // ",a,," 复制代码ES6 则是明确将空位转为
undefined。Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。Array.from(['a',,'b']) // [ "a", undefined, "b" ] 复制代码扩展运算符(
...)也会将空位转为undefined。[...['a',,'b']] // [ "a", undefined, "b" ] 复制代码copyWithin()会连空位一起拷贝。[,'a','b',,].copyWithin(2,0) // [,"a",,"a"] 复制代码fill()会将空位视为正常的数组位置。new Array(3).fill('a') // ["a","a","a"] 复制代码for...of循环也会遍历空位。let arr = [, ,]; for (let i of arr) { console.log(1); } // 1 // 1 复制代码上面代码中,数组
arr有两个空位,for...of并没有忽略它们。如果改成map方法遍历,空位是会跳过的。entries()、keys()、values()、find()和findIndex()会将空位处理成undefined。// entries() [...[,'a'].entries()] // [[0,undefined], [1,"a"]] // keys() [...[,'a'].keys()] // [0,1] // values() [...[,'a'].values()] // [undefined,"a"] // find() [,'a'].find(x => true) // undefined // findIndex() [,'a'].findIndex(x => true) // 0 复制代码由于空位的处理规则非常不统一,所以建议避免出现空位。
-
待更新





















![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)