js ES6 引用类型的新特性

目录

前言

一、字符串

1、模板字符串

(1)、一般的模板字符串

(2)、标签模板(了解)

2、ES6 对 Unicode 码点的支持

(1)、字符的 Unicode 表示法

(2)、支持 Unicode 的 for...of

(3)、支持 Unicode 的 JSON.stringify()

3、新增的方法

(1)、原型的 String.fromCodePoint() 方法

(2)、原型的 String.raw() 方法

(3)、codePointAt() 方法

(4)、normalize() 方法

(5)、子串的识别

(6)、字符串重复

(7)、字符串补全

(8)、空格的消除与补充

(9)、matchAll() 方法

二、正则

1、对 RegExp() 构造函数的优化

2、新增的修饰符

(1)、 u 修饰符

(2)、s 修饰符

(3)、y 修饰符

 3、新增表示 Unicode 字符的语法

4、新增的正则实例属性

(1)、检测是否使用了 u 修饰符、y 修饰符

(2)、检测正则表达式是否处在dotAll模式

(3)、返回正则表达式的修饰符

5、后行断言

6、具名组匹配

(1)、正则的具名组匹配与解构赋值

(2)、正则的具名组匹配与替换字符串

(3)、引用某个“具名组匹配”

7、matchAll() 方法

三、数值

1、BigInt 数据类型

2、新增指数运算符(**)

3、极小常量——Number.EPSILON

4、数值的表示

(1)、二进制表示法

(2)、八进制表示法

5、Number 对象新方法

6、Math 对象的扩展

(1)、Math.trunc()

(2)、Math.sign() 

(3)、Math.cbrt()

(4)、Math.hypot()

(5)、3个32位数值运算方法

(6)、4个对数方法

(7)、6个双曲函数方法

四、函数

1、函数参数的默认值

(1)、函数参数的默认值的特点

(2)、与解构赋值默认值结合使用

(3)、可省略的参数——将参数默认值设为undefined

(4)、参数默认值的位置

(5)、函数的 length 属性的默认值

(6)、设置了默认值的参数的作用域

2、当函数参数个数不确定时,用 rest 参数作为函数的形参

3、优化函数的 name 属性

4、箭头函数

(1)、箭头函数的两种写法

(2)、箭头函数与变量解构结合使用

(3)、箭头函数嵌套使用

(4)、箭头函数的特性

(5)、箭头函数不可以使用的场景

(6)、箭头函数的其他教程 

5、用尾调用进行代码优化

(1)、尾调用优化

(2)、尾递归

(3)、严格模式下的尾调用

(4)、尾递归的优化——蹦床函数

6、函数参数的尾逗号

7、Function.prototype.toString() 方法的变更

8、try...catch 语句的变更

五、数组

1、用扩展运算符(spread)操作数组

2、新增的两个原型上的方法

(1)、Array.from()

(2)、Array.of()

3、新增的数组实例的方法

(1)、数组实例的 copyWithin()

(2)、数组实例的 find() 和 findIndex()

(3)、数组实例的 fill()

(4)、数组实例的 entries(),keys() 和 values()

(5)、数组实例的 includes()

(6)、数组实例的 flat()

(7)、数组实例的 flatMap()

4、原有数组方法在 ES6 中的使用注意事项

(1)、forEach() 方法与 Async 函数

六、对象

1、属性的简洁表示法

(1)、直接用变量作为对象的属性

(2)、直接用变量作为对象的方法 

2、属性名表达式

(1)、什么是属性名表达式?

(2)、属性名表达式的用途

(3)、属性名表达式的注意事项

3、对象方法的 name 属性

(1)、对象的一般 name 属性

(2)、对象的特殊 name 属性

4、遍历对象的属性

(1)、对象的可枚举性

(2)、属性的遍历

5、super 关键字

(1)、super 关键字的语法

(2)、super 关键字的使用

6、对象的剩余运算符

7、链判断运算符(?.)

(1)、?.运算符简化了一种判断

(2)、?.运算符的运用

(3)、?.运算符的常用场合

(4)、?.运算符的注意事项

8、Null 判断运算符(??)

9、对象的新增方法

(1)、Object.is()

(2)、Object.assign()

(3)、Object.getOwnPropertyDescriptors()

(4)、__proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()

(5)、Object.keys(),Object.values(),Object.entries()

(6)、Object.fromEntries()


本文介绍的是 ES6 新增的引用类型的内容,若要了解 ES5 定义的引用类型的内容,请戳这里

前言

在原型上的变动,比如函数,都会是 Function.XXX;

在实例上的变动,比如函数的实例,都会是 Function.prototype.XXX。

一、字符串

1、模板字符串

(1)、一般的模板字符串

模板字符串使用反引号 `,可以在字符串中加入变量、表达式 和 函数——变量名写在 ${} 中,JavaScript 表达式 和 函数 放在 ${} 中。

模板字符串可以用来定义:普通字符串 和 多行字符串。

let userName = "Mali";
let str =  `Hello,
my name is ${Mali}`;

console.log(str);
// Hello,
// my name is Mali

(2)、标签模板(了解)

标签模板,是一个函数的调用,其中调用的参数是模板字符串。

alert`Hello world!`;
// 等价于
alert('Hello world!');

当模板字符串中带有变量,会将模板字符串参数处理成多个参数。

function f(stringArr,...values){
 let result = "";
 for(let i=0;i<stringArr.length;i++){
  result += stringArr[i];
  if(values[i]){
   result += values[i];
        }
    }
 return result;
}
let name = 'Mike';
let age = 27;
f`My Name is ${name},I am ${age+1} years old next year.`;
// "My Name is Mike,I am 28 years old next year."
 
f`My Name is ${name},I am ${age+1} years old next year.`;
// 等价于
f(['My Name is',',I am ',' years old next year.'],'Mike',28);

2、ES6 对 Unicode 码点的支持

(1)、字符的 Unicode 表示法

ES6 允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点。

关于 Unicode 码点详见:计算机编码那些事儿_weixin79893765432...的博客-CSDN博客

"\u0061"
// "a"

在 ES5 中,这种表示法只限于码点在\u0000~\uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。

但是,在ES6 中,只要将码点放入大括号,就能正确解读该字符:

"\u{20BB7}"
// "𠮷"

"\u{41}\u{42}\u{43}"
// "ABC"

let hello = 123;
hell\u{6F} // 123

'\u{1F680}' === '\uD83D\uDE80'
// true

至此,JavaScript 共有 6 种方法可以表示一个字符:

'z' === 'z'  // true
'\172' === 'z' // true
'\x7A' === 'z' // true
'\u007A' === 'z' // true
'\u{7A}' === 'z' // true

(2)、支持 Unicode 的 for...of

ES6 为字符串添加了遍历器接口,使得字符串可以被for...of循环遍历。

与传统的for循环相比,这个遍历器更大的优点是:可以识别大于0xFFFF的码点。

let text = String.fromCodePoint(0x20BB7);

for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// " "
// " "

for (let i of text) {
  console.log(i);
}
// "𠮷"

(3)、支持 Unicode 的 JSON.stringify()

UTF-8 标准规定,0xD800到0xDFFF之间的码点,不能单独使用,必须配对使用。否则,可能返回0xD800到0xDFFF之间的单个码点。

为了确保返回的是合法的 UTF-8 字符,ES2019 改变了JSON.stringify()的行为。如果遇到0xD800到0xDFFF之间的单个码点,或者不存在的配对形式,它会返回转义字符串,留给应用自己决定下一步的处理。

JSON.stringify('\u{D834}'); // ""\\uD834""
JSON.stringify('\uDF06\uD834'); // ""\\udf06\\ud834""

3、新增的方法

(1)、原型的 String.fromCodePoint() 方法

ES5 提供String.fromCharCode()方法,用于从 Unicode 码点返回对应字符,但是这个方法不能识别码点大于0xFFFF的字符。

ES6 提供了String.fromCodePoint()方法,可以识别大于0xFFFF的字符,弥补了String.fromCharCode()方法的不足。

String.fromCodePoint(0x20BB7);// "𠮷"

注意:fromCodePoint方法定义在String对象上,而codePointAt方法定义在字符串的实例对象上。

(2)、原型的 String.raw() 方法

该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,用于模板字符串的处理方法。

String.raw`Hi\u000A!`;
// 实际返回 "Hi\\u000A!",显示的是转义后的结果 "Hi\u000A!"

(3)、codePointAt() 方法

UTF-16 编码需要 4 个字节(一个字符两个字节)储存。对于这种4个字节的字符,ES6 提供了codePointAt()方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。

该方法的参数只有一个:字符在字符串中的位置(从 0 开始)。

该方法返回 32 位的 UTF-16 字符的码点。对于那些两个字节储存的常规字符,它的返回结果与charCodeAt()方法相同。

let s = '𠮷a';

s.codePointAt(0) // 134071
s.codePointAt(1) // 57271
s.codePointAt(2) // 97
s.codePointAt(3) // undefined

上面代码中,JavaScript 将“𠮷a”视为三个字符:

  • codePointAt 方法在第一个字符上,正确地识别了“𠮷”的前两个字节,返回了它的十进制码点 134071(即十六进制的20BB7)。
  • 在第二个字符(即“𠮷”的后两个字节)上,正确的识别了“𠮷”的后两个字节,返回了它的十进制码点 57271(即十六进制的0xDFB7)。
  • 在第三个字符“a”上,正确的识别了 a 的码点 97(即十六进制的0061)。
  • 由于没有第四个字符,所以返回 undefined。

(4)、normalize() 方法

该方法用来将字符的不同表示方法统一为同样的形式,也称为 Unicode 正规化。不过,该方法目前不能识别三个或三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过 Unicode 编号区间判断。

该方法可以接受一个参数,用来指定normalize的方式,参数的四个可选值如下。

  • NFC,默认参数,表示“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价。
  • NFD,表示“标准等价分解”(Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符。
  • NFKC,表示“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价,比如“囍”和“喜喜”。(这只是用来举例,normalize方法不能识别中文。)
  • NFKD,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符。
'\u004F\u030C'.normalize('NFC').length // 1
'\u004F\u030C'.normalize('NFD').length // 2

 上面代码表示,NFC参数返回字符的合成形式,NFD参数返回字符的分解形式。

(5)、子串的识别

ES6 之前判断字符串是否包含子串,用 indexOf 方法,ES6 新增了子串的识别方法:

  • includes():返回布尔值,判断是否找到参数字符串。
  • startsWith():返回布尔值,判断参数字符串是否在原字符串的头部。
  • endsWith():返回布尔值,判断参数字符串是否在原字符串的尾部。

以上三个方法都可以接受两个参数:需要搜索的字符串 和 (可选的)搜索起始位置索引。

以上三个方法都返回布尔值。如果需要知道子串的位置,还是得用 indexOf 和 lastIndexOf。

let s = 'Hello world!';

s.startsWith('Hello')       // true
s.endsWith('!')             // true
s.includes('o')             // true
s.startsWith('world', 6)    // true

(6)、字符串重复

repeat():用来将字符串重复指定次数,然后返回新的字符串。

该方法接收一个参数:重复的次数。

参数取值需要注意:

  • NaN:自动转为 0。
  • 负数:自动转为 0。
  • Infinity:自动转为 0。
  • 字符串:自动转为 0。
  • 小数:自动向下取整。
'x'.repeat(3)        // "xxx"
'hello'.repeat(0)    // ""
'boy'.repeat(2.9)    // "boyboy"

(7)、字符串补全

  • padStart:返回新的字符串,表示用参数字符串从头部(左侧)补全原字符串。
  • padEnd:返回新的字符串,表示用参数字符串从尾部(右侧)补全原字符串。

以上两个方法都可以接受两个参数:指定生成的字符串的最小长度 和 用来补全的字符串(默认用空格填充)。

参数取值需要注意:

  • 如果原字符串的长度,等于或大于最大长度,则字符串补全不生效,返回原字符串。
  • 如果用来补全的字符串与原字符串,两者的长度之和超过了最大长度,则会截去超出位数的补全字符串。 
'x'.padStart(5, 'ab')               // 'ababx'
'x'.padEnd(5, 'ab')                 // 'xabab'

'xxx'.padStart(2, 'ab')             // 'xxx'

'abc'.padStart(10, '0123456789')    // '0123456abc'

(8)、空格的消除与补充

  • trimStart()
  • trimEnd()

这两个方法的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。

这两个方法对字符串头部(或尾部)的 tab 键、换行符等不可见的空白符号也有效。

const s = '  abc  ';

s.trim() // "abc"
s.trimStart() // "abc  "
s.trimEnd() // "  abc"

(9)、matchAll() 方法

详见本文正则的 matchAll() 方法。

二、正则

1、对 RegExp() 构造函数的优化

ES5 中规定,用 构造函数法 创建正则表达式,需要传2个参数:正则表达式 (pattern)和 标志(flags),这两个参数都是字符串。

var pattern = new RegExp("[bc]at", "g");

ES6 中, 第一个参数,可以直接传入一个正则对象了。

let re = new RegExp(/abc/ig, 'i');
re; // /abc/i

上面代码中,原有正则对象的修饰符是 ig,它会被第二个参数 i 覆盖。

2、新增的修饰符

(1)、 u 修饰符

ES6 对正则表达式添加了u修饰符,含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符。也就是说,使用u修饰符后,正则表达式就能够正确处理四个字节的 UTF-16 编码了。

/^\uD83D/u.test('\uD83D\uDC2A') // false
/^\uD83D/.test('\uD83D\uDC2A') // true

(2)、s 修饰符

ES2018 引入s修饰符,使得 . 可以匹配任意单个字符。这被称为dotAll模式,即点(dot)代表一切字符。

/foo.bar/s.test('foo\nbar') // true

(3)、y 修饰符

ES6 为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。

y 修饰符的作用与 g 修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而 y 修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。

var s = 'aaa_aa_a';
var r1 = /a+/g;
var r2 = /a+/y;

r1.exec(s) // ["aaa"]
r2.exec(s) // ["aaa"]

r1.exec(s) // ["aa"]
r2.exec(s) // null

上面代码有两个正则表达式,一个使用g修饰符,另一个使用y修饰符。这两个正则表达式各执行了两次,第一次执行的时候,两者行为相同,剩余字符串都是_aa_a。由于g修饰没有位置要求,所以第二次执行会返回结果,而y修饰符要求匹配必须从头部开始,所以返回null。

y修饰符遵守lastIndex属性,但是要求必须在lastIndex指定的位置发现匹配。关于lastIndex属性,请戳此链接:js 引用类型_weixin79893765432...的博客-CSDN博客_js函数是引用类型吗

 3、新增表示 Unicode 字符的语法

ES6 新增了使用大括号表示 Unicode 字符。

不过,这种表示法在正则表达式中必须加上u修饰符,才能识别当中的大括号,否则会被解读为量词。

/\u{61}/.test('a') // false
/\u{61}/u.test('a') // true
/\u{20BB7}/u.test('𠮷') // true

4、新增的正则实例属性

  • unicode 属性:返回一个布尔值,表示是否设置了 u 修饰符。
  • sticky 属性:返回一个布尔值,表示是否设置了 y 修饰符。
  • dotAll 属性:返回一个布尔值,表示该正则表达式是否处在dotAll模式。
  • flags 属性:返回正则表达式的修饰符。

(1)、检测是否使用了 u 修饰符、y 修饰符

const r2 = /hello/u;
r2.unicode // true

var r = /hello\d/y;
r.sticky // true

(2)、检测正则表达式是否处在dotAll模式

const re = /foo.bar/s;
re.dotAll // true

(3)、返回正则表达式的修饰符

/abc/ig.flags; // 'gi'

5、后行断言

ES2018 引入后行断言,在这之前,JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。

  • “先行断言”指的是,x只有在y前面才匹配,必须写成/x(?=y)/。比如,只匹配百分号之前的数字,要写成/\d+(?=%)/。
  • “先行否定断言”指的是,x只有不在y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成/\d+(?!%)/。
  • “后行断言”正好与“先行断言”相反,x只有在y后面才匹配,必须写成/(?<=y)x/。比如,只匹配美元符号之后的数字,要写成/(?<=\$)\d+/。
  • “后行否定断言”则与“先行否定断言”相反,x只有不在y后面才匹配,必须写成/(?<!y)x/。比如,只匹配不在美元符号后面的数字,要写成/(?<!\$)\d+/。
/\d+(?=%)/.exec('100% of US presidents have been male')  // ["100"]
/\d+(?!%)/.exec('that’s all 44 of them')                 // ["44"]
/(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ["100"]
/(?<!\$)\d+/.exec('it’s is worth about €90')                // ["90"]

6、具名组匹配

正则表达式使用圆括号进行组匹配。

const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;

上面代码中,正则表达式里面有三组圆括号。使用exec方法,就可以将这三组匹配结果提取出来。

ES2018 引入了具名组匹配(Named Capture Groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。

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

上面代码中,“具名组匹配”在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名”(?<year>),然后就可以在exec方法返回结果的groups属性上引用该组名。同时,数字序号(matchObj[1])依然有效。

如果具名组没有匹配,那么对应的groups对象属性会是undefined。 

const RE_OPT_A = /^(?<as>a+)?$/;
const matchObj = RE_OPT_A.exec('');

matchObj.groups.as // undefined
'as' in matchObj.groups // true

(1)、正则的具名组匹配与解构赋值

有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。

let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar');
one  // foo
two  // bar

(2)、正则的具名组匹配与替换字符串

有了具名组匹配以后,字符串替换时,使用$<组名>引用具名组。

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}`;
});

具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。 

(3)、引用某个“具名组匹配”

 如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>的写法。

const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/;
RE_TWICE.test('abc!abc!abc') // true
RE_TWICE.test('abc!abc!ab') // false

7、matchAll() 方法

如果一个正则表达式在字符串里面有多个匹配,在 ES6 之前,一般使用g修饰符或y修饰符,在循环里面逐一取出。ES6 增加了String.prototype.matchAll()方法,可以一次性取出所有匹配。

该方法返回一个正则表达式在当前字符串的所有匹配。

const str = 'test1test2test3';
const regex = /t(e)(st(\d?))/g;

for (const match of str.matchAll(regex)) {
  console.log(match);
}
// ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"]
// ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"]
// ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]

返回的并不是数组,而是伪数组(遍历器(Iterator)),可以将它转为数组:

// 转为数组的方法一
[...string.matchAll(regex)]

// 转为数组的方法二
Array.from(string.matchAll(regex))

三、数值

1、BigInt 数据类型

ES2020 新增了 BigInt 数据类型,该类型有望会成为 JavaScript 的第 7 大原始数据类型。

BigInt 数据类型只用来表示大整数,没有位数的限制,任何位数的整数都可以精确表示。

BigInt 数据类型的详情,请戳这里:js ES2020定义了 BigInt 构造函数_weixin79893765432...的博客-CSDN博客

2、新增指数运算符(**)

ES2016 新增了一个指数运算符(**)。

2 ** 3 // 8
// 相当于
Math.pow(2, 3) // 8

多个指数运算符连用时,是从最右边开始计算的:

// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512

指数运算符可以与等号结合,形成一个新的赋值运算符(**=)。

let a = 1.5;
a **= 2;
// 等同于 a = a * a;

let b = 4;
b **= 3;
// 等同于 b = b * b * b;

3、极小常量——Number.EPSILON

ES6 在Number对象上面,新增一个极小的常量Number.EPSILON。它表示 1 与大于 1 的最小浮点数之间的差。

Number.EPSILON

浮点数计算是不精确的。引入Number.EPSILON在于为浮点数计算,设置一个误差范围。Number.EPSILON实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以忽略不计了。

0.1 + 0.2 === 0.3; // false
// 在误差范围内即视为相等
equal = (Math.abs(0.1 - 0.3 + 0.2) < Number.EPSILON); // true

4、数值的表示

(1)、二进制表示法

二进制表示法新写法: 前缀 0b 或 0B 。

0b111110111 === 503 // true
0o767 === 503 // true

(2)、八进制表示法

八进制表示法新写法: 前缀 0o 或 0O 。

console.log(0o11 === 9); // true
console.log(0O11 === 9); // true

注意,在严格模式下,八进制就不允许使用前缀0表示。

5、Number 对象新方法

  • Number.isFinite():该方法用来检查一个数值是否为有限的(finite),即不是Infinity。如果参数类型不是数值,该方法一律返回false。
  • Number.isNaN():该方法用来检查一个值是否为NaN。如果参数类型不是NaN,该方法一律返回false。
  • Number.parseInt() 和 Number.parseFloat():ES6 将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。
  • Number.isInteger():该方法用来判断一个数值是否为整数。如果参数不是数值,该方法返回false。
  • Number.isSafeInteger():该方法用来判断一个整数是否在“安全整数”范围之内。

JavaScript 的“安全整数”指的是:在 -2^53 到 2^53 之间(不含两个端点)的整数,都能被精确表示。

ES6 引入了安全整数的上限和下限两个常量:

  1. 上限(2^53):Number.MAX_SAFE_INTEGE;
  2. 下限(-2^53):Number.MIN_SAFE_INTEGER。

实际判断一个整数是否在安全整数范围之内需要封装两个函数:

Number.isSafeInteger = function (n) {
  return (typeof n==='number' && Math.round(n)===n && Number.MIN_SAFE_INTEGER<=n && n<=Number.MAX_SAFE_INTEGER);
}

function trusty (left, right, result) {
  if (
    Number.isSafeInteger(left) && Number.isSafeInteger(right) && Number.isSafeInteger(result)
  ) {
    return result;
  }
  throw new RangeError('Operation cannot be trusted!');
}

trusty(9007199254740993, 990, 9007199254740993 - 990)
// RangeError: Operation cannot be trusted!

trusty(1, 2, 3)
// 3

6、Math 对象的扩展

ES6 在 Math 对象上新增了 17 个与数学相关的方法:

(1)、Math.trunc()

该方法用于去除一个数的小数部分,返回整数部分。

该方法的参数取值分析:

  • 对于非数值,该方法内部自动使用Number方法将其先转为数值。
  • 对于空值和无法截取整数的值,返回NaN。
Math.trunc('123.456') // 123
Math.trunc(true) //1
Math.trunc(false) // 0

Math.trunc(null) // 0
Math.trunc(NaN);      // NaN
Math.trunc('foo');    // NaN
Math.trunc();         // NaN
Math.trunc(undefined) // NaN

若没有部署这个方法的环境,可以用下面的代码模拟:

Math.trunc = Math.trunc || function(x) {
  return x < 0 ? Math.ceil(x) : Math.floor(x);
};

(2)、Math.sign() 

该方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。

该方法的参数取值分析:

  • 参数为正数,返回+1;
  • 参数为负数,返回-1;
  • 参数为 0,返回0;
  • 参数为-0,返回-0;
  • 其他值,返回NaN;
  • 如果参数是非数值,会自动转为数值,无法转为数值的值,会返回NaN。
Math.sign(-5) // -1
Math.sign(5) // +1
Math.sign(0) // +0
Math.sign(-0) // -0
Math.sign(NaN) // NaN

Math.sign('')  // 0
Math.sign(true)  // +1
Math.sign(false)  // 0
Math.sign(null)  // 0
Math.sign('9')  // +1
Math.sign('foo')  // NaN
Math.sign()  // NaN
Math.sign(undefined)  // NaN

若没有部署这个方法的环境,可以用下面的代码模拟:

Math.sign = Math.sign || function(x) {
  x = +x; // convert to a number
  if (x === 0 || isNaN(x)) {
    return x;
  }
  return x > 0 ? 1 : -1;
};

(3)、Math.cbrt()

该方法用于计算一个数的立方根。

该方法的参数取值分析:

  • 对于非数值,Math.cbrt()方法内部也是先使用Number()方法将其转为数值。
Math.cbrt(-1) // -1
Math.cbrt(2)  // 1.2599210498948732
Math.cbrt('8') // 2
Math.cbrt('hello') // NaN

若没有部署这个方法的环境,可以用下面的代码模拟:

Math.cbrt = Math.cbrt || function(x) {
  var y = Math.pow(Math.abs(x), 1/3);
  return x < 0 ? -y : y;
};

(4)、Math.hypot()

该方法返回所有参数的平方和的平方根。

该方法的参数取值分析:

  • 如果参数不是数值,Math.hypot方法会将其转为数值。
  • 如果参数是NaN,就无法转为数值,会返回 NaN。
Math.hypot(3, 4);        // 5
Math.hypot(3, 4, 5);     // 7.0710678118654755
Math.hypot();            // 0
Math.hypot(NaN);         // NaN
Math.hypot(3, 4, 'foo'); // NaN
Math.hypot(3, 4, '5');   // 7.0710678118654755
Math.hypot(-3);          // 3

(5)、3个32位数值运算方法

①、Math.clz32()

将参数转为 32 位无符号整数的形式,然后返回这个 32 位值里面有多少个前导 0。

该方法的参数取值分析:

  • 左移运算符(<<)与Math.clz32方法直接相关。
  • 对于小数,Math.clz32方法只考虑整数部分。
  • 对于空值或其他类型的值,Math.clz32方法会将它们先转为数值,然后再计算。
Math.clz32(1) // 31
Math.clz32(1 << 1) // 30
Math.clz32(3.2) // 30

Math.clz32() // 32
Math.clz32(NaN) // 32
Math.clz32(Infinity) // 32
Math.clz32(null) // 32
Math.clz32('foo') // 32
Math.clz32([]) // 32
Math.clz32({}) // 32
Math.clz32(true) // 31

②、Math.imul()

返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。

Math.imul(2, 4)   // 8
Math.imul(-1, 8)  // -8

如果只考虑最后 32 位,大多数情况下,Math.imul(a, b)与a * b的结果是相同的,即该方法等同于(a * b)|0的效果(超过 32 位的部分溢出)。之所以需要部署这个方法,是因为 JavaScript 有精度限制,超过 2 的 53 次方的值无法精确表示。这就是说,对于那些很大的数的乘法,低位数值往往都是不精确的,Math.imul方法可以返回正确的低位数值。

(0x7fffffff * 0x7fffffff)|0 // 0

上面这个乘法算式,返回结果为 0。但是由于这两个二进制数的最低位都是 1,所以这个结果肯定是不正确的,因为根据二进制乘法,计算结果的二进制最低位应该也是 1。这个错误就是因为它们的乘积超过了 2 的 53 次方,JavaScript 无法保存额外的精度,就把低位的值都变成了 0。Math.imul方法可以返回正确的值 1。

Math.imul(0x7fffffff, 0x7fffffff) // 1

③、Math.fround()

该方法主要用来将64位双精度浮点数转为32位单精度浮点数。如果小数的精度超过24个二进制位,返回值就会不同于原值,否则返回值不变(即与64位双精度值一致)。

 该方法的参数取值分析:

  • 在 24 个二进制之内的小数,经该方法运算后,返回值与64位双精度值一致;超过 24 个二进制的小数,经该方法运算后,返回值就会不同于原值:
// 在 24 个二进制之内的小数
Math.fround(0)   // 0
Math.fround(1)   // 1
Math.fround(2 ** 24 - 1)   // 16777215

// 超过 24 个二进制的小数
Math.fround(2 ** 24)       // 16777216
Math.fround(2 ** 24 + 1)   // 16777216
  • 对于 NaN 和 Infinity,此方法返回原值。
  • 对于其它类型的非数值,Math.fround 方法会先将其转为数值,再返回单精度浮点数。
Math.fround(NaN)      // NaN
Math.fround(Infinity) // Infinity

Math.fround('5')      // 5
Math.fround(true)     // 1
Math.fround(null)     // 0
Math.fround([])       // 0
Math.fround({})       // NaN

若没有部署这个方法的环境,可以用下面的代码模拟:

Math.fround = Math.fround || function (x) {
  return new Float32Array([x])[0];
};

(6)、4个对数方法

  • Math.expm1():返回 ex - 1,即Math.exp(x) - 1。若没有部署这个方法的环境,可以用下面的代码模拟:
Math.expm1 = Math.expm1 || function(x) {
  return Math.exp(x) - 1;
};
  • Math.log1p():返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN。若没有部署这个方法的环境,可以用下面的代码模拟:
Math.log1p = Math.log1p || function(x) {
  return Math.log(1 + x);
};
  • Math.log10():返回以 10 为底的x的对数。如果x小于 0,则返回 NaN。若没有部署这个方法的环境,可以用下面的代码模拟:
Math.log10 = Math.log10 || function(x) {
  return Math.log(x) / Math.LN10;
};
  • Math.log2():返回以 2 为底的x的对数。如果x小于 0,则返回 NaN。若没有部署这个方法的环境,可以用下面的代码模拟:
Math.log2 = Math.log2 || function(x) {
  return Math.log(x) / Math.LN2;
};

(7)、6个双曲函数方法

  • Math.sinh(x):返回x的双曲正弦(sine);
  • Math.cosh(x):返回x的双曲余弦(cosine);
  • Math.tanh(x):返回x的双曲正切(tangent);
  • Math.asinh(x):返回x的反双曲正弦;
  • Math.acosh(x):返回x的反双曲余弦;
  • Math.atanh(x):返回x的反双曲正切。

四、函数

1、函数参数的默认值

ES6 之前,不能直接为函数的参数指定默认值。ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

function log(x, y = 'World') {
  console.log(x, y);
}

log('Hello') // Hello World
log('Hello', 'China') // Hello China

(1)、函数参数的默认值的特点

  • 使用参数默认值时,函数不能有同名参数。
  • 参数默认值是惰性求值的。

①、使用参数默认值时,函数不能有同名参数

// 不报错
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。 

(2)、与解构赋值默认值结合使用

注意理解和区分:“对象的解构赋值默认值” 与 “函数参数的默认值”(有点绕)。

①、只使用了对象的解构赋值默认值:

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

②、只使用了函数参数的默认值:

function fn({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}
fn();// [0, 0]

③、当同时设置了“函数参数的默认值” 和 “对象的解构赋值默认值”,函数参数的默认值会先生效,然后才是解构赋值的默认值生效,最终返回的是后生效的。

function m2({x=1, y=2} = { x: 0, y: 0 }) {
  return [x, y];
}
m2();// [0, 0]

④、请问下面两种写法有什么差别?

// 写法一
function m1({x = 0, y = 0} = {}) {
  return [x, y];
}

// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
  return [x, y];
}

上面两种写法都对函数的参数设定了默认值,区别是:

  • 写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;
  • 写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。

(3)、可省略的参数——将参数默认值设为undefined

可以将参数默认值设为undefined,表明这个参数是可以省略的。

function foo(optional = undefined) { ··· }

(4)、参数默认值的位置

定义了默认值的参数,最好放在函数的参数的末尾。因为这样比较容易看出来,到底省略了哪些参数。

如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

// 例一
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。

(5)、函数的 length 属性的默认值

指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

(6)、设置了默认值的参数的作用域

一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(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。

如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。

let foo = 'outer';

function bar(func = () => foo) {
  let foo = 'inner';
  console.log(func());
}

bar(); // outer

上面代码中,函数bar的参数func的默认值是一个匿名函数,返回值为变量foo。函数参数形成的单独作用域里面,并没有定义变量foo,所以foo指向外层的全局变量foo,因此输出outer。

下面是一个更复杂的例子:

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

注意下面这种写法会报错:

var x = 1;

function foo(x = x) {
  // ...
}

foo() // ReferenceError: x is not defined

上面代码中,参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“。

2、当函数参数个数不确定时,用 rest 参数作为函数的形参

当函数参数个数不确定时,用 rest 参数 作为函数的形参,会把未来传入函数的参数整合成一个数组。

function fn(...arr){
    console.log(arr);
}

fn(1, 2, 3, 4, 5, 6);
// [1, 2, 3, 4, 5, 6]

关于 rest 参数 的进一步学习,请戳此查看

3、优化函数的 name 属性

函数的name属性,返回该函数的函数名。

function foo() {}
foo.name // "foo"

 函数的 name 属性的特性:

  • 如果将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。
  • 如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字。
  • Function构造函数返回的函数实例,name属性的值为anonymous。
  • bind返回的函数,name属性值会加上bound前缀。

如果将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。

var f = function () {};

// ES5
f.name // ""

// ES6
f.name // "f"

如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字。

const bar = function baz() {};

// ES5
bar.name // "baz"

// ES6
bar.name // "baz"

Function构造函数返回的函数实例,name属性的值为anonymous。

let fn = new Function();
fn.name // "anonymous"

bind返回的函数,name属性值会加上bound前缀。

function foo() {};
foo.bind({}).name // "bound foo"

(function(){}).bind({}).name // "bound "

4、箭头函数

ES6 允许使用“箭头”(=>)定义函数。

(1)、箭头函数的两种写法

  • 只包含一个表达式时可以省略 花括号 和 return。
  • 包含多条语句时不能省略 花括号 和 return。

例如:

// 写法一:只包含一个表达式时可以省略 花括号 和 return。
const f = v => v;

// 写法二:包含多条语句时不能省略 花括号 和 return。
const f = (x, y, ...rest) => {
  let sum = x + y;
  for (let i=0; i<rest.length; i++) {
      sum += rest[i];
  }
  return sum;
};

以下定义箭头函数的方式都是正确的:

  • 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
  • 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
  • 由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
var f = () => 5;
var sum = (num1, num2) => num1 + num2;
var sum = (num1, num2) => { return num1 + num2; }
let getTempItem = id => ({ id: id, name: "Temp" });

(2)、箭头函数与变量解构结合使用

const full = ({ first, last }) => first + ' ' + last;

(3)、箭头函数嵌套使用

let insert = (value) => ({into: (array) => ({after: (afterValue) => {
  array.splice(array.indexOf(afterValue) + 1, 0, value);
  return array;
}})});

insert(2).into([1, 3]).after(1); //[1, 2, 3]

(4)、箭头函数的特性

当要求动态上下文的时候,就不能够使用箭头函数,也就是this的固定化。

  • 箭头函数不会改变this指向。
  • 箭头函数不能使用call()、apply()、bind()这些方法改变this,因为它的this是固定不变的。
  • 箭头函数不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
  • 箭头函数中不存在arguments对象。如果要用,可以用 rest 参数代替。详情请戳此查看
  • 箭头函数不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

①、箭头函数不会改变 this 指向

箭头函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。箭头函数的this是固定的,换而言之,箭头函数使得this从“动态”变成了“静态”。

function foo() {
  setTimeout(() => {
    console.log('id:', this.id);
  }, 100);
}

var id = 21;

foo.call({ id: 42 });
// id: 42

 上述代码可知:箭头函数可以让setTimeout里面的this,绑定定义时所在的作用域,而不是指向运行时所在的作用域。

(5)、箭头函数不可以使用的场景

  • 用构造函数创建函数时不可以使用箭头函数。
  • 定义对象的方法且该方法内部包括 this 时不可以使用箭头函数。
  • 需要动态 this 的时候不可以使用箭头函数。
  • 原型上的函数不可以使用箭头函数。

1⃣️、用构造函数创建函数时不可以使用箭头函数

const Person = () => {
    Person.prototype.name = "marry";
    Person.prototype.sayName = function(){
        console.log(this.name);
    }
}
 
const person1 = new Person();

// Uncaught TypeError: Person is not a constructor

2⃣️、定义对象的方法且该方法内部包括 this 时不可以使用箭头函数

const calculator = {
    array: [1, 2, 3],
    sum: () => {
        console.log(this === window); // => true
        return this.array.reduce((result, item) => result + item);
    }
};

console.log(this === window); // => true

calculator.sum();

// true
// true
// Uncaught TypeError: Cannot read property 'reduce' of undefined

3⃣️、原型上的函数不可以使用箭头函数

function Cat(name) {
    this.name = name;
}

Cat.prototype.sayCatName = () => {
    console.log(this === window); // => true
    return this.name;
};

const cat = new Cat('Mike');
cat.sayCatName(); 

// ""

上述代码虽然未报错,但是返回的值显然是不对的,应该返回 Mike 却返回了空字符串。

(6)、箭头函数的其他教程 

什么时候你不能使用箭头函数?——王仕军

廖雪峰的箭头函数教程

5、用尾调用进行代码优化

“尾调用” 指的是:某个函数的最后一步是调用另一个函数。

(1)、尾调用优化

“尾调用优化” 就是:只保留内层函数的调用帧。

“尾调用优化”的意义:如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。

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)的调用帧。

注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;
  }
  return inner(a);
}

上面的函数不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one。

(2)、尾递归

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

比如:使用尾递归优化 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

(3)、严格模式下的尾调用

ES6 的严格模式下,箭头函数的尾调用才有效。

在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈:

  • arguments 对象:返回调用时函数的参数。
  • caller 对象:返回调用当前函数的那个函数(调用者)。

严格模式下,arguments 和 caller 对象都不能使用,因为尾调用优化发生时,函数的调用栈会改写。

function stricted() {
  'use strict';
  stricted.caller;    // 报错
  stricted.arguments; // 报错
}
stricted();

(4)、尾递归的优化——蹦床函数

尾递归优化只在严格模式下生效。

尾递归之所以需要优化,原因是调用栈太多,造成溢出。比如:

function sum(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1);
  } else {
    return x;
  }
}

sum(1, 100000)
// Uncaught RangeError: Maximum call stack size exceeded(…)

上面代码中,sum是一个递归函数,参数x是需要累加的值,参数y控制递归次数。一旦指定sum递归 100000 次,就会报错,提示超出调用栈的最大次数。

①、使用蹦床函数优化代码

蹦床函数(trampoline)可以将递归执行转为循环执行。

function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}

蹦床函数是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题。

下面,用蹦床函数优化一下sum递归函数:

// 蹦床函数
function trampoline(f) {
  while (f && f instanceof Function) {
    f = f();
  }
  return f;
}

/**
 * 首先,改写sum函数:
 *     将原来的递归函数,改写不是递归的函数——为每一步返回另一个函数,尾调用这个函数。
 */ 
function sum(x, y) {
  if (y > 0) {
    return sum.bind(null, x + 1, y - 1);
  } else {
    return x;
  }
}

// 然后,使用蹦床函数执行sum
trampoline(sum(1, 100000))
// 100001

这样,就不会发生调用栈溢出。

可是,这样修改了之后,sum就不是尾递归了。

②、改造蹦床函数优化尾递归

蹦床函数并不是真正的尾递归优化,下面的实现才是:

function tco(f) {
  var value;
  var active = false;
  var accumulated = [];

  return function accumulator() {
    accumulated.push(arguments);
    if (!active) {
      active = true;
      while (accumulated.length) {
        value = f.apply(this, accumulated.shift());
      }
      active = false;
      return value;
    }
  };
}

var sum = tco(function(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1)
  }
  else {
    return x
  }
});

sum(1, 100000)
// 100001

上面代码中,tco函数是尾递归优化的实现,它的奥妙就在于状态变量active。默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一轮递归sum返回的都是undefined,所以就避免了递归执行;而accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了accumulator函数内部的while循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。

6、函数参数的尾逗号

ES2017 允许函数的最后一个参数有尾逗号。

function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

7、Function.prototype.toString() 方法的变更

ES2019 对函数实例的toString()方法做出了修改。
toString() 方法用来返回函数代码本身。修改前该方法会省略注释和空格;修改后的该方法,明确要求返回一模一样的原始代码。

function /* foo comment */ foo () {}

foo.toString()
// 修改前返回:function foo() {}
// 修改后返回:"function /* foo comment */ foo () {}"

8、try...catch 语句的变更

JavaScript 语言的try...catch语句,以前明确要求catch命令后面必须跟参数,接受try代码块抛出的错误对象。

try {
  // ...
} catch (err) {
  // 处理错误
}

ES2019 规定,catch 命令后面的参数可以省略了。

try {
  // ...
} catch {
  // ...
}

五、数组

1、用扩展运算符(spread)操作数组

用扩展运算符操作数组,请戳这里:js ES6扩展运算符(spread)和剩余运算符(rest)_weixin79893765432...的博客-CSDN博客

2、新增的两个原型上的方法

(1)、Array.from()

该方法接收 3 个参数:一个待转为数组的对象、(可选的)一个map 函数 和(可选的)map 函数执行时的 this 对象。

  • 第一个参数:必须是“类数组对象”“可迭代对象”
    • 类数组对象:具有 length 属性的对象。
    • 可迭代对象:定义了遍历器(Iterator)接口的对象。常见的 可迭代对象 包括:
      • Array
      • Map
      • Set
      • String
      • 函数中的 arguments 对象
      • NodeList 对象
      • Generator 函数
  • 第二个参数:用于对每个元素进行处理,放入数组的是处理后的元素。
  • 第三个参数:用于指定 map 函数执行时的 this 对象。

该方法用来,将“类数组对象”或“可迭代对象”转化为数组。

①、当接收一个参数时

Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']

②、当接收两个参数时

Array.from([1, 2, 3], (x) => x * x)
// [1, 4, 9]

③、当接收三个参数时

let map = {
    do: function(n) {
        return n * 2;
    }
}
let arrayLike = [1, 2, 3];

Array.from(arrayLike, function(n){return this.do(n);}, map);
// [2, 4, 6]

注意,上述代码中,Array.from() 方法的第二个参数,不能使用箭头函数,否则报错:this.do is not a function。 

④、Array.from() 方法 与 扩展运算符

扩展运算符(...)也可以将“可迭代对象”转为数组,不过,它不能将“类数组对象”转为数组。而 Array.from() 方法就可以弥补这种不足。

/**
 * 案例一
 */ 
// 扩展运算符
console.log(...{length: 3});// TypeError: Found non-callable @@iterator

// Array.from() 方法
console.log(Array.from({ length: 3 }));// [undefined, undefined, undefined]


/**
 * 案例二
 */ 
var arrayLike = {
    a: "111",
    b: "222",
    c: "333"
};

// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

// ES6的写法
let arr3 = [...arrayLike];// TypeError: object is not iterable (cannot read property Symbol(Symbol.iterator))

let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']

扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,可以考虑采用 Array.from 方法转换。Array.from方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。

至此,JavaScript 中,将对象转为数组的方式有三种了:

  • Array.prototype.slice.call():ES5中将“类数组对象”或“可迭代对象”转化为数组。
  • 扩展运算符:ES6中将“可迭代对象”转化为数组。
  • Array.from():ES6中将“类数组对象”或“可迭代对象”转化为数组。
var str = 'hello';

// ES5的写法
var arr1 = [].slice.call(str);     // ["h", "e", "l", "l", "o"]

// ES6的写法
let arr2 = Array.from(str);        // ["h", "e", "l", "l", "o"]

let arr3 = [...str];               // ["h", "e", "l", "l", "o"]

对于还没有部署 Array.from() 方法的浏览器,可以用 Array.prototype.slice() 方法替代

const toArray = (() =>
  Array.from ? Array.from : obj => [].slice.call(obj)
)();

注意:别用  Array.from() 方法将对象转为数组,详见案例:

// 没有length属性的对象会被转为空数组
let obj = {
    a: "1",
    b: "2"
};
console.log(Array.from(obj));     // []
console.log(...obj);              // TypeError: Found non-callable @@iterator

// 有length属性的对象会被转为包含length个undefined的数组
let obj = {
    a: "1",
    b: "2",
    length: 2
};
console.log(Array.from(obj));     // [undefined, undefined]
console.log(...obj);              // TypeError: Found non-callable @@iterator

(2)、Array.of()

该方法参数有不定数个,参数是一组值。

该方法用于将一组值转换为数组,并返回该数组。如果没有参数,就返回一个空数组。

Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]

对于还没有部署 Array.of() 方法的浏览器,可以用下面的代码模拟实现。

function ArrayOf(){
  return [].slice.call(arguments);
}

3、新增的数组实例的方法

(1)、数组实例的 copyWithin()

该方法在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回修改后的数组。

如果当前数组为空,就返回空数组。

let a1 = [].copyWithin(1, 0, 3);

该方法可以接收 3 个参数:

Array.prototype.copyWithin(target, start = 0, end = this.length)
  • target(必需):从该位置开始替换数据。如果为负值,表示从末尾开始计算。
  • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示从末尾开始计算。
  • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示从末尾开始计算。如果大于数组长度,就按数组长度算。

这三个参数都应该是数值,如果不是,会自动转为数值。

①、数组内一个元素替换另一个元素:

// 将3号位复制到0号位
[1, 2, 3, 4, 5].copyWithin(0, 3, 4);// [4, 2, 3, 4, 5]

// 将3号位复制到-1号位(-1号位也就是4号位)
[1, 2, 3, 4, 5].copyWithin(-1, 3, 4);// [1, 2, 3, 4, 4]

// 将3号位复制到0号位
[].copyWithin.call({length: 5, 3: 1}, 0, 3)
// {0: 1, 3: 1, length: 5}

②、对象内一个元素替换另一个元素:

[].copyWithin.call({length: 5, 3: 1}, 0, 3);// {0: 1, 3: 1, length: 5}

// 相当于
({0:undefined,1:undefined,2:undefined,3: 1,4:undefined,5:undefined,length: 5}).copyWithin(0,3,5);

// 结果为:
// {0:1,1:undefined,2:undefined,3: 1,4:undefined,5:undefined,length: 5};
// 也就是:
// {0:1,3:1,length:5}

(2)、数组实例的 find() 和 findIndex()

  • find() 方法用来,找出第一个符合条件的数组成员,然后返回该成员。如果没有符合条件的成员,则返回 undefined。
  • findIndex() 方法的用来,找出第一个符合条件的数组成员的位置,然后返回该位置。如果所有成员都不符合条件,则返回 -1。

这两个方法都可以接收 2 个参数:一个回调函数 和 (可选的)用来绑定回调函数的 this 对象。所有数组成员依次执行该回调函数。

①、当接收一个参数时

[1, 4, -5, 10].find((n) => n < 0)
// -5

②、当接收两个参数时

function f(v){
  return v > this.age;
}
let person = {name: 'John', age: 20};
[10, 12, 26, 15].find(f, person);    // 26

③、这两个方法都可以发现 NaN

这两个方法都可以发现 NaN,弥补了数组的 indexOf 方法的不足。 

// ES5 中
[NaN].indexOf(NaN);// -1

// ES6 中
[NaN].find(y => Object.is(NaN, y));// NaN

[NaN].findIndex(y => Object.is(NaN, y));// 0

(3)、数组实例的 fill()

该方法使用给定值,填充一个数组。

该方法可以接收 3 个参数:填充的值、(可选的)填充的起始位置 和 (可选的)填充的结束位置。

①、接收一个参数时:

['a', 'b', 'c'].fill(7)
// [7, 7, 7]

new Array(3).fill(7)
// [7, 7, 7]

②、接收多个参数时:

['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']

③、如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。

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]]

(4)、数组实例的 entries(),keys() 和 values()

entries()、keys() 和 values() 这三个方法用于遍历数组。它们都返回一个遍历器对象(Iterator),可以用 for...of 循环进行遍历,唯一的区别是:

  • keys() 是对 键名 的遍历;
  • values() 是对 值 的遍历;
  • entries() 是对 键值对 的遍历。
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"

如果不使用 for...of 循环,可以手动调用遍历器对象的 next() 方法,进行遍历。

let arr = ['a', 'b', 'c'];
let entries = arr.entries();
console.log(entries.next().value); // [0, 'a']
console.log(entries.next().value); // [1, 'b']
console.log(entries.next().value); // [2, 'c']

(5)、数组实例的 includes()

该方法返回一个布尔值,表示某个数组是否包含给定的值。与字符串的 includes() 方法类似。

①、includes() 方法的语法

该方法可以接收 2 个参数:一个给定的值 和 (可选的)搜索的起始位置。

  • 如果第二个参数为负数,则表示倒数的位置;
  • 如果第二个参数的绝对值大于等于数组长度,负数的话会重置为从 0 开始,否则该方法返回 false。

--> 接收一个参数时:

[1, 2, 3].includes(2)     // true
[1, 2, 3].includes(4)     // false
[1, 2, NaN].includes(NaN) // true

--> 接收两个参数时:

[1, 2, 3].includes(3, 2); // true
[1, 2, 3].includes(3, -2); // true

[1, 2, 3].includes(3, 3); // false
[1, 2, 3].includes(3, -3); // true

②、includes() 方法与 Map 和 Set 数据结构的 has() 方法

另外,Map 和 Set 数据结构有一个has方法,需要注意与includes区分:

  • Map 结构的has方法,是用来查找 键名 的,比如:Map.prototype.has(key)。
  • Set 结构的has方法,是用来查找 值 的,比如:Set.prototype.has(value)。

③、includes() 方法与 indexOf() 方法

没有 includes() 方法之前,我们通常使用数组的 indexOf() 方法,检查是否包含某个值。

if (arr.indexOf(el) !== -1) {
  // ...
}

不过,indexOf() 方法有两个缺点:

  • 不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。
  • 它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。

而 includes() 方法就没有这样的问题。

④、如果浏览器不支持 includes() 方法,就用下面的代码兼容一下

const contains = (() =>
  Array.prototype.includes
    ? (arr, value) => arr.includes(value)
    : (arr, value) => arr.some(el => el === value)
)();
contains(['foo', 'bar'], 'baz'); // => false

(6)、数组实例的 flat()

flat() 方法,用于将嵌套的数组“扁平化”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

该方法可以接收一个参数:(可选的)一个整数,表示想要拉平的层数。默认值为 1。如果不管有多少层嵌套,都要转成一维数组,可以用 Infinity 关键字作为参数。

[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]

[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]

[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]

[1, [2, [3]]].flat(Infinity)
// [1, 2, 3]

如果原数组有空位,flat()方法会跳过空位。

[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]

(7)、数组实例的 flatMap()

flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]

flatMap()只能展开一层数组。

// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])
// [[2], [4], [6], [8]]

该方法可以接收 2 个参数:一个遍历函数 和 (可选的)绑定遍历函数的 this。

传入的遍历函数可以接受三个参数:

  • 当前数组成员;
  • 当前数组成员的位置(从零开始);
  • 原数组。
arr.flatMap(function callback(currentValue, index, array) {
  // ...
}, thisArg)

4、原有数组方法在 ES6 中的使用注意事项

(1)、forEach() 方法与 Async 函数

// 延迟 num 秒后 打印 num 
const say = async (num) => {
  console.log(num, 'begin:')
  await new Promise(resolve => {
    setTimeout(() => {
      console.log(num)
      resolve()
    }, num * 1000)
  })
}

const nums = [2, 1]

// 遍历 nums 打印
async function for_Result() {
  for (let n of nums) {
    await say(n)
  }
}

// 遍历 nums 打印
function forEach_Result() {
  nums.forEach(async n => {
    await say(n)
  })
}


// for_Result()
// 2 begin:
// 1 begin:
// 1
// 2

// forEach_Result()
// 2 begin:
// 1 begin:
// 1
// 2

由上述代码可知,使用 for...of 或 forEach 结合 Async 函数遍历数组,最终控制台输出的结果是一致的,但是执行过程是大相径庭的—— forEach 并没有按照预期的去等待 say() 方法执行完毕。这是为什么呢?我们来看一下 for...of 与 forEach 在 Async 函数中执行的差异:

  • for of会在遍历到每个元素后,执行say()方法。
  • forEach在遍历每个元素后,执行的是该方法接收的回调函数方法,然而,forEach方法内部的 回调函数 并没有使用 await 修饰,所以回调方法并不会等待上一个回调执行完毕。forEach 里的 await 也就失去了意义。

六、对象

1、属性的简洁表示法

ES6 允许在大括号里面,直接写入变量和函数,作为对象的属性和方法。

(1)、直接用变量作为对象的属性

const foo = 'bar';
const baz = {foo};
baz // {foo: "bar"}

// 等同于
const baz = {foo: foo};

(2)、直接用变量作为对象的方法 

const o = {
  method() {
    return "Hello!";
  }
};

// 等同于

const o = {
  method: function() {
    return "Hello!";
  }
};

2、属性名表达式

(1)、什么是属性名表达式?

JavaScript 定义对象的属性,有两种方法:

var obj = {};

// 方法一
obj.foo = true;
// 方法二
obj['a' + 'bc'] = 123;

console.log(obj);// {foo: true, abc: 123}

(2)、属性名表达式的用途

如果使用字面量方式定义对象(使用大括号):

  • 在 ES5 中只能使用方法一(标识符)定义属性。
  • ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。
// ES5 中
var obj = {
  foo: true,
  abc: 123
};

// ES6 中
let propKey = 'foo';

let obj = {
  [propKey]: true,
  ['a' + 'bc']: 123
};

属性名表达式还可以用于定义方法名。

let obj = {
  ['h' + 'ello']() {
    return 'hi';
  }
};

obj.hello() // hi

(3)、属性名表达式的注意事项

  • 使用了 “属性名表达式”,就不能使用 “属性的简洁表示法” 了,两者不能同时使用,否则会报错。
  • 属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object] 。

使用了 “属性名表达式”,就不能使用 “属性的简洁表示法” 了,两者不能同时使用,否则会报错。

// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};

属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object] 。

// 报错
const foo = 'bar';
const bar = 'abc';
const baz = { [foo] };

// 正确
const foo = 'bar';
const baz = { [foo]: 'abc'};

3、对象方法的 name 属性

(1)、对象的一般 name 属性

函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。

const person = {
  sayName() {
    console.log('hello!');
  },
};

person.sayName.name   // "sayName"

(2)、对象的特殊 name 属性

  • 对象的方法使用了取值函数(getter)和存值函数(setter)时,name 属性在对象的get和set属性上面,返回值是方法名前加上get和set。
  • bind方法创造的函数,name属性返回bound加上原函数的名字。
  • Function构造函数创造的函数,name属性返回anonymous。
  • 如果对象的方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。

对象的方法使用了取值函数(getter)和存值函数(setter)时,name 属性在对象的get和set属性上面,返回值是方法名前加上get和set。

const obj = {
  get foo() {},
  set foo(x) {}
};

obj.foo.name
// TypeError: Cannot read property 'name' of undefined

const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo');

descriptor.get.name // "get foo"
descriptor.set.name // "set foo"

bind方法创造的函数,name属性返回bound加上原函数的名字;Function构造函数创造的函数,name属性返回anonymous。

(new Function()).name // "anonymous"

var doSomething = function() {
  // ...
};
doSomething.bind().name // "bound doSomething"

如果对象的方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。

const key1 = Symbol('description');
const key2 = Symbol();
let obj = {
  [key1]() {},
  [key2]() {},
};
obj[key1].name // "[description]"
obj[key2].name // ""

4、遍历对象的属性

(1)、对象的可枚举性

对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

let obj = { foo: 123 };
Object.getOwnPropertyDescriptor(obj, 'foo')
//  {
//    value: 123,
//    writable: true,
//    enumerable: true,
//    configurable: true
//  }

描述对象的enumerable属性,称为“可枚举性”,如果该属性为false,就表示某些操作会忽略当前属性。

目前,有四个操作会忽略enumerable为false的属性。

  • for...in循环:只遍历对象自身的和继承的可枚举的属性。
  • Object.keys():返回对象自身的所有可枚举的属性的键名。
  • JSON.stringify():只串行化对象自身的可枚举的属性。
  • Object.assign(): 忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。

另外,ES6 规定,所有 Class 的原型的方法都是不可枚举的。

(2)、属性的遍历

ES6 一共有 5 种方法可以遍历对象的属性:

  • for...in:循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。
  • Object.keys:返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。
  • Object.getOwnPropertyNames:返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
  • Object.getOwnPropertySymbols:返回一个数组,包含对象自身的所有 Symbol 属性的键名。
  • Reflect.ownKeys:返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则:

  • 首先遍历所有数值键,按照数值升序排列。
  • 其次遍历所有字符串键,按照加入时间升序排列。
  • 最后遍历所有 Symbol 键,按照加入时间升序排列。
Reflect.ownKeys({ [Symbol()]:0, b:0, 10:0, 2:0, a:0 })
// ['2', '10', 'b', 'a', Symbol()]

5、super 关键字

ES6 新增了 super 关键字,与 this 关键字类似,去又不同:

  • this 关键字总是指向函数所在的当前对象。
  • super 关键字指向的是当前对象的原型对象。

(1)、super 关键字的语法

因为super 关键字指向的是当前对象的原型对象,所以,super.XXX 等同于 ES5 中的 “Object.getPrototypeOf(this).XXX——属性” 或 “Object.getPrototypeOf(this).XXX.call(this)——方法”。以属性为例:

const proto = {
  foo: 'hello'
};

const obj = {
  foo: 'world',
  find() {
    return super.foo;
  }
};

Object.setPrototypeOf(obj, proto);

Object.getPrototypeOf(obj, proto).foo;    // "hello"
obj.find();                               // "hello"

(2)、super 关键字的使用

super 关键字只能用在对象方法的 简洁表示法 之中。用在对象的 属性 上会报错,用在对象方法的 非简洁表示法 中也会报错。

①、super 用在对象的属性上

// 报错
const proto = {
  foo: 'hello'
};

const obj = {
  find: super.foo
}
Object.setPrototypeOf(obj, proto);
console.log(obj.find());                   // SyntaxError: 'super' keyword unexpected here

②、super 用在对象的方法上

const proto = {
  foo: 'hello'
};

// 对象方法的简洁表示法中——正常
const obj = {
    find(){
        return super.foo;
    }
}
Object.setPrototypeOf(obj, proto);
obj.find();                             // "hello"

// 报错
const obj1 = {
  find: () => super.foo
}
Object.setPrototypeOf(obj1, proto);
obj1.find();                            // SyntaxError: 'super' keyword unexpected here

// 报错
const obj2 = {
  find: function () {
    return super.foo
  }
}
Object.setPrototypeOf(obj2, proto);
obj2.find();                            // SyntaxError: 'super' keyword unexpected here

6、对象的剩余运算符

为什么说是对象的剩余运算符,而不说是对象的扩展运算符呢?因为:

  • 扩展运算符:将一个数组转为用逗号分隔的参数序列,简而言之就是,将 “可迭代对象” 展开。
  • 剩余运算符:将逗号隔开的值序列组合成一个 数组 或 对象。

例如:用剩余运算符解构对象

let { x, ...y } = { x: 1, a: 2, b: 3 };
x // 1
y // { a: 3, b: 3 }

console.log(...y);// TypeError: Found non-callable @@iterator

有上述代码可知,利用剩余运算符得到的对象不一定是 “可迭代对象”。

用剩余运算符解构对象作为函数的形参:

function baseFunction({ a, b }) {
  // ...
}
function wrapperFunction({ x, y, ...restConfig }) {
  // 使用 x 和 y 参数进行操作
  // 其余参数传给原始函数
  return baseFunction(restConfig);
}

对于剩余运算符的详细讲解,请戳此链接:js ES6新特性_weixin79893765432...的博客-CSDN博客

7、链判断运算符(?.)

(1)、?.运算符简化了一种判断

ES2020 引入了“链判断运算符”——“?.”。

链判断运算符的作用:在读取对象内部的某个属性,或者调用对象内部的某个方法之前,用链判断运算符判断:该对象是否存在;以及要读取的属性是否存在,或者要调用的方法是否存在。

比如:要读取 message.body.user.firstName 属性。在链判断运算符诞生之前,会这样读取:

// 错误的写法
const  firstName = message.body.user.firstName;

// 正确的写法
const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || 'default';

三元运算符(也叫三目运算符) “? :” 也常用于判断对象是否存在。比如,获取名字是foo的input元素里的值:

const fooInput = myForm.querySelector('input[name=foo]')
const fooValue = fooInput ? fooInput.value : undefined

 链判断运算符诞生后,使得这种判断更加简便了:

const firstName = message?.body?.user?.firstName || 'default';
const fooValue = myForm.querySelector('input[name=foo]')?.value

(2)、?.运算符的运用

①、链判断运算符,判断对象的属性是否存在

用链判断运算符判断对象的属性是否存在时,有两种写法:

  • 直接 obj?. 属性;
  • 或者 obj?. [属性]。

比如,要读取 message.body.user.firstName 属性:

const firstName = message?.body?.user?.firstName || 'default';
// 或者
const firstName = message?.[body]?.[user]?.[firstName] || 'default';

②、链判断运算符,判断函数或对象方法是否存在

if (myForm.checkValidity?.() === false) {
  // 表单校验失败
  return;
}

(3)、?.运算符的常用场合

下面是?.运算符常见形式,以及不使用该运算符时的等价形式。

a?.b
// 等同于
a == null ? undefined : a.b

a?.[x]
// 等同于
a == null ? undefined : a[x]

a?.b()
// 等同于
a == null ? undefined : a.b()

a?.()
// 等同于
a == null ? undefined : a()

(4)、?.运算符的注意事项

①、短路机制

?.运算符相当于一种短路机制,只要不满足条件,就不再往下执行。

a?.[++x]
// 等同于
a == null ? undefined : a[++x]

②、delete 运算符

delete a?.b
// 等同于
a == null ? undefined : delete a.b

上面代码中,如果a是undefined或null,会直接返回undefined,而不会进行delete运算。 

③、圆括号的影响

一般来说,使用?.运算符的场合,不应该使用圆括号。

如果属性链有圆括号,链判断运算符对圆括号外部没有影响,只对圆括号内部有影响。

(a?.b).c
// 等价于
(a == null ? undefined : a.b).c

④、报错场合

以下写法是禁止的,会报错。

// 构造函数
new a?.()
new a?.b()

// 链判断运算符的右侧有模板字符串
a?.`{b}`
a?.b`{c}`

// 链判断运算符的左侧是 super
super?.()
super?.foo

// 链运算符用于赋值运算符左侧
a?.b = c

⑤、右侧不得为十进制数值

为了保证兼容以前的代码,允许foo?.3:0被解析成foo ? .3 : 0,因此规定如果?.后面紧跟一个十进制数字,那么?.不再被看成是一个完整的运算符,而会按照三元运算符进行处理,也就是说,那个小数点会归属于后面的十进制数字,形成一个小数。

foo?.3:0
// 右侧为十进制数值时,会被默认解析为三目运算
foo ? 0.3 : 0

8、Null 判断运算符(??)

读取对象属性的时候,如果某个属性的值是null或undefined,有时候需要为它们指定默认值。常见做法是通过||运算符指定默认值。

const headerText = response.settings.headerText || 'Hello, world!';

上面的三行代码都通过||运算符指定默认值,但是这样写是错的。开发者的原意是,只要属性的值为null或undefined,默认值就会生效,但是属性的值如果为空字符串或false或0,默认值也会生效。

为了避免这种情况,ES2020 引入了一个新的 Null 判断运算符??。

?? 的行为类似 ||,但是 ?? 只有运算符左侧的值为 null 或 undefined 时,才会返回右侧的值。

const headerText = response.settings.headerText ?? 'Hello, world!';

?? 的一个目的是,与链判断运算符 ?. 配合使用,为 null 或 undefined 的值设置默认值。

const headerText = response.settings?.headerText ?? 'Hello, world!';

?? 很适合判断函数参数是否赋值。

function Component(props) {
  const enable = props.enabled ?? true;
  // …
}
// 等同于
function Component(props) {
  const {
    enabled: enable = true,
  } = props;
  // …
}

?? 有一个运算优先级问题,它与&&和||的优先级孰高孰低。现在的规则是,如果多个逻辑运算符一起使用,必须用括号表明优先级,否则会报错。

// 报错
lhs && middle ?? rhs
lhs ?? middle && rhs
lhs || middle ?? rhs
lhs ?? middle || rhs

// 正常
(lhs && middle) ?? rhs;
lhs && (middle ?? rhs);

(lhs ?? middle) && rhs;
lhs ?? (middle && rhs);

(lhs || middle) ?? rhs;
lhs || (middle ?? rhs);

(lhs ?? middle) || rhs;
lhs ?? (middle || rhs);

9、对象的新增方法

(1)、Object.is()

Object.is() 方法,用来比较两个值是否严格相等。使用的算法叫做“Same-value-zero equality”。

ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和 严格相等运算符(===)。它们都有缺点:

  • 相等运算符(==):会自动转换数据类型;
  • 严格相等运算符(===):NaN不等于自身,以及+0等于-0。

JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。于是 Object.is() 方法诞生了。

Object.is('foo', 'bar');     // false
Object.is('foo', 'foo');     // true
Object.is(window, window);   // true

var foo = { a: 1 };
var bar = { a: 1 };
Object.is(foo, foo);         // true
Object.is(foo, bar);         // false

Object.is([], []);           // false
Object.is({}, {});           // false
Object.is(null, null);       // true

Object.is() 方法与严格相等运算符(===)的不同之处有两个:

  • +0 不等于 -0;
  • 二是 NaN 等于自身。
+0 === -0 //true
NaN === NaN // false

Object.is(0, -0);            // false
Object.is(+0, -0);           // false
Object.is(0, +0);            // true
Object.is(-0, -0);           // true
Object.is(NaN, 0/0);         // true
Object.is(NaN, NaN);         // true

ES5 可以通过下面的代码,部署Object.is。

Object.defineProperty(Object, 'is', {
    value: function(x, y) {
        if (x === y) {
            // 针对+0 不等于 -0的情况
            return x !== 0 || 1 / x === 1 / y;
        }
        // 针对NaN的情况
        return x !== x && y !== y;
    },
    configurable: true,
    enumerable: false,
    writable: true
});

(2)、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}

--> 该方法的特性:

  • 如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
  • 如果只有一个参数,Object.assign会直接返回该参数。
  • 如果首参不是对象,则会先转成对象,然后返回。
  • undefined 和 null 无法转为对象,在首参会报错,在非首参会被忽略掉过。
  • 如果数值、字符串和布尔值作为非首参,不会报错,字符串会以数组形式,拷贝入目标对象;其他类型值都不会产生效果。
  • Object.assign 只能拷贝源对象的自身属性,不拷贝继承属性,也不拷贝不可枚举的属性(enumerable: false)。
  • 属性名为 Symbol 值的属性,也会被Object.assign拷贝。
  • Object.assign方法实行的是浅拷贝,而不是深拷贝。
  • Object.assign可以用来处理数组,但是会把数组视为对象。
  • Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。

①、如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

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会直接返回该参数。

const obj = {a: 1};
Object.assign(obj) === obj // true

③、如果首参不是对象,则会先转成对象,然后返回。

console.log(Object.assign(2));            // Number {2}
console.log(typeof Object.assign(2));     // "object"

④、undefined 和 null 无法转为对象,在首参会报错,在非首参会被忽略掉过。

// 报错
Object.assign(undefined)
Object.assign(null)

// 忽略跳过
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true

⑤、如果数值、字符串和布尔值作为非首参,不会报错,字符串会以数组形式,拷贝入目标对象;其他类型值都不会产生效果。

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" }

⑥、Object.assign 只能拷贝源对象的自身属性,不拷贝继承属性,也不拷贝不可枚举的属性(enumerable: false)。

Object.assign({b: 'c'},
  Object.defineProperty({}, 'invisible', {
    enumerable: false,
    value: 'hello'
  })
)
// { b: 'c' }

⑦、属性名为 Symbol 值的属性,也会被Object.assign拷贝。

Object.assign({ a: 'b' }, { [Symbol('c')]: 'd' })
// { a: 'b', Symbol(c): 'd' }

⑧、Object.assign方法实行的是浅拷贝,而不是深拷贝。

 如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

const obj1 = {a: {b: 1}};
const obj2 = Object.assign({}, obj1);

obj1.a.b = 2;
obj2.a.b // 2

⑨、Object.assign可以用来处理数组,但是会把数组视为对象。

Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]

 上面代码中,Object.assign把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1。

⑩、Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。

const source = {
  get foo() { return 1 }
};
const target = {};

Object.assign(target, source)
// { foo: 1 }

上面代码中,source对象的foo属性是一个取值函数,Object.assign不会复制这个取值函数,只会拿到值以后,将这个值复制过去。

 -->Object.assign() 常见用途

  • 为对象添加属性。
  • 覆盖(修改)原有对象的属性值。
  • 为对象添加方法。
  • 克隆对象(以及保持继承链的克隆对象)。
  • 合并多个对象。
  • 为属性指定默认值。

①、为对象添加属性

const obj = {a: 1};

Object.assign(obj, {b: 2});

console.log(obj);// {a: 1, b: 2}

上面方法通过Object.assign方法,将x属性和y属性添加到Point类的对象实例。 

②、覆盖(修改)原有对象的属性值

const obj = {a: 1};

Object.assign(obj, {a: 2});

console.log(obj);// {a: 2}

③、为对象添加方法

Object.assign(SomeClass.prototype, {
  someMethod(arg1, arg2) {
    ···
  },
  anotherMethod() {
    ···
  }
});

// 等同于下面的写法
SomeClass.prototype.someMethod = function (arg1, arg2) {
  ···
};
SomeClass.prototype.anotherMethod = function () {
  ···
};

 上面代码使用了对象属性的简洁表示法,直接将两个函数放在大括号中,再使用assign方法添加到SomeClass.prototype之中。

④、克隆对象

function clone(origin) {
    return Object.assign({}, origin);
}

上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。

不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。

保持继承链的克隆对象方法

function clone(origin) {
    let originProto = Object.getPrototypeOf(origin);
    return Object.assign(Object.create(originProto), origin);
}

⑤、合并多个对象

将多个对象合并到某个对象。

const merge = (target, ...sources) => Object.assign(target, ...sources);

如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。

const merge = (...sources) => Object.assign({}, ...sources);

⑥、为属性指定默认值

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对象的该属性很可能不起作用。 

(3)、Object.getOwnPropertyDescriptors()

ES5 的Object.getOwnPropertyDescriptor()方法会返回某个对象属性的描述对象(descriptor)。

ES2017 引入了Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。

const obj = {
  foo: 123,
  get bar() { return 'abc' }
};

Object.getOwnPropertyDescriptors(obj)
// { foo:
//    { value: 123,
//      writable: true,
//      enumerable: true,
//      configurable: true },
//   bar:
//    { get: [Function: get bar],
//      set: undefined,
//      enumerable: true,
//      configurable: true } }

对于还没有部署 Object.getOwnPropertyDescriptors() 方法的浏览器,可以用下面的代码模拟实现。

function getOwnPropertyDescriptors(obj) {
  const result = {};
  for (let key of Reflect.ownKeys(obj)) {
    result[key] = Object.getOwnPropertyDescriptor(obj, key);
  }
  return result;
}

Object.getOwnPropertyDescriptors()方法的用处:

  • 用Object.getOwnPropertyDescriptors()方法解决Object.assign()无法正确拷贝get属性和set属性的问题。
  • 用Object.getOwnPropertyDescriptors()方法配合Object.create()方法,将对象属性克隆到一个新对象。这属于浅拷贝。
  • 用Object.getOwnPropertyDescriptors()方法实现一个对象继承另一个对象。
  • 用Object.getOwnPropertyDescriptors()方法来实现 Mixin(混入)模式。

①、用Object.getOwnPropertyDescriptors()方法解决Object.assign()无法正确拷贝get属性和set属性的问题。

比如:Object.assign()拷贝set属性失败:

const source = {
  set foo(value) {
    console.log(value);
  }
};

const target1 = {};
Object.assign(target1, source);

Object.getOwnPropertyDescriptor(target1, 'foo')
// { value: undefined,
//   writable: true,
//   enumerable: true,
//   configurable: true }

上面代码中,source对象的foo属性的值是一个赋值函数,Object.assign方法将这个属性拷贝给target1对象,结果该属性的值变成了undefined。这是因为Object.assign方法总是拷贝一个属性的值,而不会拷贝它背后的赋值方法或取值方法。

这时,Object.getOwnPropertyDescriptors()方法配合Object.defineProperties()方法,就可以实现正确拷贝。

const source = {
  set foo(value) {
    console.log(value);
  }
};

const target2 = {};
Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
Object.getOwnPropertyDescriptor(target2, 'foo')
// { get: undefined,
//   set: [Function: set foo],
//   enumerable: true,
//   configurable: true }

上面代码中,两个对象合并的逻辑可以写成一个函数。

const shallowMerge = (target, source) => Object.defineProperties(
  target,
  Object.getOwnPropertyDescriptors(source)
);

②、用Object.getOwnPropertyDescriptors()方法配合Object.create()方法,将对象属性克隆到一个新对象。这属于浅拷贝。

const clone = Object.create(Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj));

// 或者

const shallowClone = (obj) => Object.create(
  Object.getPrototypeOf(obj),
  Object.getOwnPropertyDescriptors(obj)
);

③、用Object.getOwnPropertyDescriptors()方法实现一个对象继承另一个对象。

以前,继承另一个对象,常常写成下面这样:

const obj = {
  __proto__: prot,
  foo: 123,
};

ES6 规定__proto__只有浏览器要部署,其他环境不用部署。如果去除__proto__,上面代码就要改成下面这样。

const obj = Object.create(prot);
obj.foo = 123;

// 或者

const obj = Object.assign(
  Object.create(prot),
  {
    foo: 123,
  }
);

有了Object.getOwnPropertyDescriptors(),我们就有了另一种写法。

const obj = Object.create(
  prot,
  Object.getOwnPropertyDescriptors({
    foo: 123,
  })
);

④、用Object.getOwnPropertyDescriptors()方法来实现 Mixin(混入)模式。

let mix = (object) => ({
  with: (...mixins) => mixins.reduce(
    (c, mixin) => Object.create(
      c, Object.getOwnPropertyDescriptors(mixin)
    ), object)
});

// multiple mixins example
let a = {a: 'a'};
let b = {b: 'b'};
let c = {c: 'c'};
let d = mix(c).with(a, b);

d.c // "c"
d.b // "b"
d.a // "a"

上面代码返回一个新的对象d,代表了对象a和b被混入了对象c的操作。

出于完整性的考虑,Object.getOwnPropertyDescriptors()进入标准以后,以后还会新增Reflect.getOwnPropertyDescriptors()方法。

(4)、__proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()

①、__proto__属性

__proto__属性(前后各两个下划线),用来读取或设置当前对象的原型对象(prototype)。

// es5 的写法
const obj = {
  method: function() { ... }
};
obj.__proto__ = someOtherObj;

// es6 的写法
var obj = Object.create(someOtherObj);
obj.method = function() { ... };

该属性由于被浏览器广泛支持,才被加入了 ES6。ES6 明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。

实现上,__proto__调用的是Object.prototype.__proto__,具体实现如下。

Object.defineProperty(Object.prototype, '__proto__', {
  get() {
    let _thisObj = Object(this);
    return Object.getPrototypeOf(_thisObj);
  },
  set(proto) {
    if (this === undefined || this === null) {
      throw new TypeError();
    }
    if (!isObject(this)) {
      return undefined;
    }
    if (!isObject(proto)) {
      return undefined;
    }
    let status = Reflect.setPrototypeOf(this, proto);
    if (!status) {
      throw new TypeError();
    }
  },
});

function isObject(value) {
  return Object(value) === value;
}

 如果一个对象本身部署了__proto__属性,该属性的值就是对象的原型。

Object.getPrototypeOf({ __proto__: null })
// null

②、Object.setPrototypeOf()

Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的原型对象(prototype),返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。

// 格式
Object.setPrototypeOf(object, prototype)

// 用法
const o = Object.setPrototypeOf({}, null);

该方法等同于下面的函数。

function setPrototypeOf(obj, proto) {
  obj.__proto__ = proto;
  return obj;
}

 下面是一个例子。

let proto = {};
let obj = { x: 10 };
Object.setPrototypeOf(obj, proto);

proto.y = 20;
proto.z = 40;

obj.x // 10
obj.y // 20
obj.z // 40

上面代码将proto对象设为obj对象的原型,所以从obj对象可以读取proto对象的属性。

如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果。

Object.setPrototypeOf(1, {}) === 1 // true
Object.setPrototypeOf('foo', {}) === 'foo' // true
Object.setPrototypeOf(true, {}) === true // true

由于undefined和null无法转为对象,所以如果第一个参数是undefined或null,就会报错。

Object.setPrototypeOf(undefined, {})
// TypeError: Object.setPrototypeOf called on null or undefined

Object.setPrototypeOf(null, {})
// TypeError: Object.setPrototypeOf called on null or undefined

③、Object.getPrototypeOf()

该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。

Object.getPrototypeOf(obj);

下面是一个例子。

function Rectangle() {
  // ...
}

const rec = new Rectangle();

Object.getPrototypeOf(rec) === Rectangle.prototype
// true

Object.setPrototypeOf(rec, Object.prototype);
Object.getPrototypeOf(rec) === Rectangle.prototype
// false

如果参数不是对象,会被自动转为对象。

// 等同于 Object.getPrototypeOf(Number(1))
Object.getPrototypeOf(1)
// Number {[[PrimitiveValue]]: 0}

// 等同于 Object.getPrototypeOf(String('foo'))
Object.getPrototypeOf('foo')
// String {length: 0, [[PrimitiveValue]]: ""}

// 等同于 Object.getPrototypeOf(Boolean(true))
Object.getPrototypeOf(true)
// Boolean {[[PrimitiveValue]]: false}

Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf('foo') === String.prototype // true
Object.getPrototypeOf(true) === Boolean.prototype // true

如果参数是undefined或null,它们无法转为对象,所以会报错。

Object.getPrototypeOf(null)
// TypeError: Cannot convert undefined or null to object

Object.getPrototypeOf(undefined)
// TypeError: Cannot convert undefined or null to object

(5)、Object.keys(),Object.values(),Object.entries()

①、Object.keys()

ES5 引入了Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。

var obj = { foo: 'bar', baz: 42 };
Object.keys(obj)
// ["foo", "baz"]

 ES2017 引入了跟Object.keys配套的Object.values和Object.entries,作为遍历一个对象的补充手段,供for...of循环使用。

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)属性的键值。

const obj = { foo: 'bar', baz: 42 };
Object.values(obj)
// ["bar", 42]

 返回数组的成员顺序有一定的规则,详情请戳这里:Object.keys()是按什么顺序返回值的? - SegmentFault 思否

Object.values只返回对象自身的可遍历属性。 

const obj = Object.create({}, {p: {value: 42}});
Object.values(obj) // []

上面代码中,Object.create方法的第二个参数添加的对象属性(属性p),如果不显式声明,默认是不可遍历的,因为p的属性描述对象的enumerable默认是false,Object.values不会返回这个属性。只要把enumerable改成true,Object.values就会返回属性p的值。

const obj = Object.create({}, {p:
  {
    value: 42,
    enumerable: true
  }
});
Object.values(obj) // [42]

Object.values会过滤属性名为 Symbol 值的属性。

Object.values({ [Symbol()]: 123, foo: 'abc' });
// ['abc']

 如果Object.values方法的参数是一个字符串,会返回各个字符组成的一个数组。

Object.values('foo')
// ['f', 'o', 'o']

上面代码中,字符串会先转成一个类似数组的对象。字符串的每个字符,就是该对象的一个属性。因此,Object.values返回每个属性的键值,就是各个字符组成的一个数组。

如果参数不是对象,Object.values会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values会返回空数组。

Object.values(42) // []
Object.values(true) // []

③、Object.entries()

Object.entries()方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。

const obj = { foo: 'bar', baz: 42 };
Object.entries(obj)
// [ ["foo", "bar"], ["baz", 42] ]

除了返回值不一样,该方法的行为与Object.values基本一致。

如果原对象的属性名是一个 Symbol 值,该属性会被忽略。

Object.entries({ [Symbol()]: 123, foo: 'abc' });
// [ [ 'foo', 'abc' ] ]

Object.entries的基本用途是遍历对象的属性。

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结构。

const obj = { foo: 'bar', baz: 42 };
const map = new Map(Object.entries(obj));
map // Map { foo: "bar", baz: 42 }

 手写Object.entries方法:

// 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;
}

(6)、Object.fromEntries()

Object.fromEntries()方法是Object.entries()的逆操作,用于将一个键值对数组转为对象。

Object.fromEntries([
  ['foo', 'bar'],
  ['baz', 42]
])
// { foo: "bar", baz: 42 }

该方法的主要目的,是将键值对的数据结构还原为对象,因此特别适合将 Map 结构转为对象。

// 例一
const entries = new Map([
  ['foo', 'bar'],
  ['baz', 42]
]);

Object.fromEntries(entries)
// { foo: "bar", baz: 42 }

// 例二
const map = new Map().set('foo', true).set('bar', false);
Object.fromEntries(map)
// { foo: true, bar: false }

该方法的一个用处是配合URLSearchParams对象,将查询字符串转为对象。

Object.fromEntries(new URLSearchParams('foo=bar&baz=qux'))
// { foo: "bar", baz: "qux" }

【参考文章】

阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版

阮一峰 ES6 入门教程

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值