目录
(3)、支持 Unicode 的 JSON.stringify()
(1)、原型的 String.fromCodePoint() 方法
2、当函数参数个数不确定时,用 rest 参数作为函数的形参
7、Function.prototype.toString() 方法的变更
(2)、数组实例的 find() 和 findIndex()
(4)、数组实例的 entries(),keys() 和 values()
(3)、Object.getOwnPropertyDescriptors()
(4)、__proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()
(5)、Object.keys(),Object.values(),Object.entries()
本文介绍的是 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 引入了安全整数的上限和下限两个常量:
- 上限(2^53):Number.MAX_SAFE_INTEGE;
- 下限(-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" }
【参考文章】