资讯专栏INFORMATION COLUMN

ES6学习笔记2—各扩展

Zoom / 476人阅读

摘要:字符串的扩展字符的表示法允许采用形式表示一个字符,其中表示字符的码点。返回布尔值,表示参数字符串是否在源字符串的头部。使用和这两个常量,用来表示这个范围的上下限。对于那些无法用个二进制位精确表示的小数,方法返回最接近这个小数的单精度浮点数。

字符串的扩展 字符的 Unicode 表示法

JavaScript 允许采用uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点。这种表示法只限于码点在u0000~uFFFF之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。ES6中只要将码点放入大括号,就能正确解读该字符。

"uD842uDFB7"
// "?"

"u{20BB7}"
// "?"

大括号表示法与 UTF-16 编码是等价的。

"z" === "z"  // true
"172" === "z" // true
"x7A" === "z" // true
"u007A" === "z" // true
"u{7A}" === "z" // true
字符串方法

JavaScript内部,字符以UTF-16的格式储存,每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode码点大于0xFFFF的字符),JavaScript会认为它们是两个字符,字符串长度会误判为2。

var s = "?";

s.length // 2
s.charAt(0) // ""
s.charAt(1) // ""
s.charCodeAt(0) // 55362
s.charCodeAt(1) // 57271
s.codePointAt(0) // 134071  codePointAt方法在第一个字符上,正确地识别了“?”
s.codePointAt(1) // 57271   第二个字符是“?”的后两个字节
String.fromCharCode(0x20BB7) // "ஷ" 最高位2被舍弃了,最后返回码点U+0BB7对应的字符,而不是码点U+20BB7对应的字符。
String.fromCodePoint(0x20BB7) // "?"
"?".at(0) // "?"

对于Unicode码点大于0xFFFF的字符:

charAt:无法读取整个字符。该方法不能识别码点大于0xFFFF的字符。

charCodeAt:只能分别返回前两个字节和后两个字节的值。

fromCharCode:不能识别大于0xFFFF的码点。

codePointAt:能够正确处理4个字节储存的字符,返回一个字符的码点。codePointAt方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。

fromCodePoint:可以识别0xFFFF的字符。

at: 可以识别Unicode编号大于0xFFFF的字符,返回正确的字符。这个方法可以通过垫片库实现。

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

其他方法

normalize():用来将字符的不同表示方法统一为同样的形式

includes():返回布尔值,表示是否找到了参数字符串。

startsWith():返回布尔值,表示参数字符串是否在源字符串的头部。

endsWith():返回布尔值,表示参数字符串是否在源字符串的尾部。

padStart():用于头部补全。常见用途是为数值补全指定位数和提示字符串格式。

"1".padStart(10, "0") // "0000000001"
"12".padStart(10, "YYYY-MM-DD") // "YYYY-MM-12"

padEnd():用于尾部补全。

repeat():返回一个新字符串,表示将原字符串重复n次。参数如果是小数,会被取整。

参数是负数或者Infinity,会报错。

参数是0到-1之间的小数,则等同于0,这是因为会先进行取整运算。

参数0到-1之间的小数,取整以后等于-0,repeat视同为0。

参数NaN等同于0。

repeat的参数是字符串,则会先转换成数字。

"na".repeat(2.9) // "nana"
"na".repeat(Infinity)// RangeError
"na".repeat(NaN) // ""
"na".repeat("na") // ""
"na".repeat("3") // "nanana"
字符串的遍历器接口

ES6为字符串添加了遍历器接口(详见《Iterator》一章),使得字符串可以被for...of循环遍历。该遍历可以识别大于0xFFFF的码点,传统的for循环无法识别这样的码点。

var text = String.fromCodePoint(0x20BB7);

for (let i = 0; i < text.length; i++) {
  console.log(text[i]);
}
// " "
// " "
//for循环会认为它包含两个字符(都不可打印)
for (let i of text) {
  console.log(i);
}
// "?"
//for...of循环会正确识别出这一个字符
模板字符串

模板字符串用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。在模板字符串中需要使用反引号,则前面要用反斜杠转义。使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

// 普通字符串
`In JavaScript "
" is a line-feed.`

// 多行字符串
`In JavaScript this is
 not legal.`

// 字符串中嵌入变量
`Hello ${name}, how are you ${time}?`

模板字符串中嵌入变量,需要将变量名写在${}之中。大括号内部可以放入任意的JavaScript表达式,可以进行运算,以及引用对象属性和调用函数。如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString方法。
如果模板字符串中的变量没有声明,将报错。

// 变量place没有声明
var msg = `Hello, ${place}`;
// 报错

由于模板字符串的大括号内部,就是执行JavaScript代码,因此如果大括号内部是一个字符串,将会原样输出。

`Hello ${"World"}`
// "Hello World"
标签模板

模板字符串紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。即模板字符串就是该函数的参数。标签模板是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。

//模板字符里面有变量,会将模板字符串先处理成多个参数,再调用函数。
var a = 5;
var b = 10;

tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(["Hello ", " world ", ""], 15, 50);

模板处理函数的第一个参数(模板字符串数组),还有一个raw属性。

console.log(`123`) //123
console.log`123` // ["123", raw: Array[1]]

上面代码中,第二个console.log接受的参数,实际上是一个数组。该数组有一个raw属性,保存的是转义后的原字符串。

String.raw方法,往往用来充当模板字符串的处理函数,返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串。如果原字符串的斜杠已经转义,那么String.raw不会做任何处理。

String.raw`Hi
${2+3}!`;
// "Hi
5!"
String.raw`Hi
`
// "Hi
"

String.raw方法也可以作为正常的函数使用。这时,它的第一个参数,应该是一个具有raw属性的对象,且raw属性的值应该是一个数组。

String.raw({ raw: "test" }, 0, 1, 2);
// "t0e1s2t"

// 等同于
String.raw({ raw: ["t","e","s","t"] }, 0, 1, 2);
正则的扩展 RegExp构造函数
var regex = new RegExp("xyz", "i");
// 等价于
var regex = /xyz/i;
// 等价于
var regex = new RegExp(/xyz/i);

ES5中以下写法会报错。ES6可以使用第二个参数指定修饰符,新指定的修饰符会覆盖原有的正则表达式的修饰符。

var regex = new RegExp(/xyz/ig, "i");
//原有正则对象的修饰符是ig,它会被第二个参数i覆盖.
u修饰符

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

/^uD83D/u.test("uD83DuDC2A") // false
/^uD83D/.test("uD83DuDC2A") // true

上面的代码中,uD83DuDC2A是一个四字节的UTF-16编码,代表一个字符。不加“u”,会按 ES5 将其识别为2个字符,加了“u”之后,会按 ES6 将其正确识别为一个字符。

以下几种情况就必须加上“u”才能正确识别:

.在正则表达式中表示除行终止符(换行符(n),回车符(r),行分隔符,段分隔符)外的任意单个字符,S表示匹配所有不是空格的字符。他们均正确识别码点大于0xFFFF的Unicode字符,必须加上u修饰符才能正确识别。

/^S$/.test("?") // false
/^S$/u.test("?") // true

ES6新增了使用大括号表示Unicode字符,这种表示法在正则表达式中必须加上u修饰符,才能识别。否则大括号会被解读为量词。

/^u{3}$/.test("uuu") // true 被解读为量词
/^u{3}$/u.test("uuu") // false 被解读为Unicode表达式

有些Unicode字符的编码不同,但是字型很相近,需要加u才能识别。比如,u004B与u212A都是大写的K。

/[a-z]/i.test("u212A") // false  该行代码不加u修饰符,就无法识别非规范的K字符
/[a-z]/iu.test("u212A") // true

y 修饰符

y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始。y修饰符号就是让头部匹配的标志^在全局匹配中都有效。

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

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

r2.exec(s) // ["aaa"]
r2.exec(s) // null  第一次执行后,剩余字符串是_aa_a,y修饰符要求匹配必须从头部开始,所以返回null

在split方法中使用y修饰符,原字符串必须以分隔符开头。这也意味着,只要匹配成功,数组的第一个成员肯定是空字符串。

sticky属性

ES6的正则对象多了sticky属性,表示是否设置了y修饰符。

var r = /hellod/y;
r.sticky // true
flags属性

ES6为正则表达式新增了flags属性,会返回正则表达式的修饰符。

// ES5的source属性  返回正则表达式的正文
/abc/ig.source
// "abc"

// ES6的flags属性  返回正则表达式的修饰符
/abc/ig.flags
// "gi"
先行断言

JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead)。
”先行断言“指的是,x只有在y前面才匹配,必须写成/x(?=y)/。比如,只匹配百分号之前的数字,要写成/d+(?=%)/。”先行否定断言“指的是,x只有不在y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成/d+(?!%)/。

数值的扩展

ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。可使用Number方法将0b和0o前缀的字符串数值转为十进制。

Number("0b111")  // 7
新增方法

Number.isFinite():用来检查一个数值是否为有限的(finite),对于非数值一律返回false。

Number.isFinite(0.8); // true
Number.isFinite(NaN); // false
Number.isFinite(Infinity); // false
Number.isFinite(-Infinity); // false
Number.isFinite("foo"); // false
Number.isFinite("15"); // false
Number.isFinite(true); // false

Number.isNaN():用来检查一个值是否为NaN。

Number.isNaN(NaN) // true
Number.isNaN(15) // false
Number.isNaN("15") // false
Number.isNaN(true) // false
Number.isNaN(9/NaN) // true
Number.isNaN("true"/0) // true
Number.isNaN("true"/"true") // true

ES6将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。

Number.parseInt === parseInt // true

Number.isInteger():用来判断一个值是否为整数。需要注意的是,在JavaScript内部,整数和浮点数是同样的储存方法,所以3和3.0被视为同一个值

Number.isInteger(25) // true
Number.isInteger(25.0) // true
Number.isInteger(25.1) // false
Number.isInteger("15") // false
Number.isInteger(true) // false

Number.isSafeInteger():用来判断一个整数是否落在Number.MAX_SAFE_INTEGER与Number.MIN_SAFE_INTEGER范围之内。使用该函数时,需注意不仅要验证运算结果是否落在安全整数的范围内,还要同时验证参与运算的每个值,否则很可能得到错误结果。

Number.isSafeInteger(3) // true
Number.isSafeInteger(1.2) // false

新增常量

Number.EPSILON:极小常量。用于为浮点数计算,设置一个误差范围。

JavaScript能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6使用Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

Math.pow(2, 53) // 9007199254740992
Number.MAX_SAFE_INTEGER === 9007199254740991 // true
Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER // true
Number.MIN_SAFE_INTEGER === -9007199254740991 // true
Math对象的扩展

ES6在Math对象上新增了17个与数学相关的方法。所有这些方法都是静态方法,只能在Math对象上调用。

Math.trunc:用于去除一个数的小数部分,返回整数部分。对于非数值,Math.trunc内部使用Number方法将其先转为数值。对于空值和无法截取整数的值,返回NaN。

Math.trunc("123.456")  // 123
Math.trunc(NaN);      // NaN
Math.trunc("foo");    // NaN
Math.trunc();         // NaN

Math.cbrt():用于计算一个数的立方根。对于非数值,Math.cbrt方法内部先使用Number方法将其转为数值。

Math.sign():用来判断一个数到底是正数、负数、还是零。正数返回+1,负数返回-1,0返回0,-0返回-0,其他值,返回NaN。

Math.clz32():JavaScript的整数使用32位二进制形式。Math.clz32()返回一个数的32位无符号整数形式有多少个前导0。对于小数,只考虑其整数部分。对于空值或其他类型的值,会将它们先转为数值,然后再计算。

//1000的二进制形式是0b1111101000,一共有10位,所以32位之中有22个前导0。
Math.clz32(1000) // 22

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

Math.fround:返回一个数的单精度浮点数形式。对于整数来说,Math.fround方法返回结果一样。对于那些无法用64个二进制位精确表示的小数,Math.fround方法返回最接近这个小数的单精度浮点数。

Math.fround(1)     // 1
Math.fround(1.337) // 1.3370000123977661
Math.fround(1.5)   // 1.5

Math.hypot:返回所有参数的平方和的平方根。如果参数不是数值,Math.hypot方法会将其转为数值。只要有一个参数无法转为数值,就会返回NaN。

Math.sign():用来判断一个值的正负,但是如果参数是-0,它会返回-0。

新增对数方法

Math.expm1():

Math.log1p(x):返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN。

Math.log10(x):返回以10为底的x的对数。如果x小于0,则返回NaN。

Math.log2(x):返回以2为底的x的对数。如果x小于0,则返回NaN。

新增三角函数方法

Math.sinh(x):返回x的双曲正弦(hyperbolic sine)

Math.cosh(x):返回x的双曲余弦(hyperbolic cosine)

Math.tanh(x): 返回x的双曲正切(hyperbolic tangent)

Math.asinh(x): 返回x的反双曲正弦(inverse hyperbolic sine)

Math.acosh(x): 返回x的反双曲余弦(inverse hyperbolic cosine)

Math.atanh(x): 返回x的反双曲正切(inverse hyperbolic tangent)

指数运算符

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

2 ** 3 // 8
b **= 3; // 等同于 b = b * b * b;

在 V8 引擎中,指数运算符与Math.pow的实现不相同,对于特别大的运算结果,两者会有细微的差异。

Math.pow(99, 99)
// 3.697296376497263e+197

99 ** 99
// 3.697296376497268e+197
数组的扩展 Array.from()

Array.from方法用于将两类对象转为真正的数组:类似数组的对象(即有length属性的对象)和可遍历(iterable)的对象(包括ES6新增的数据结构Set和Map)。如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。

let arrayLike = {
    "0": "a",
    "1": "b",
    "2": "c",
    length: 3
};

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

// ES6的写法
let arr2 = Array.from(arrayLike); // ["a", "b", "c"]

扩展运算符(...)也可以将某些数据结构转为数组。扩展运算符背后调用的是遍历器接口(Symbol.iterator)。

// arguments对象
function foo() {
  var args = [...arguments];
}

// NodeList对象
[...document.querySelectorAll("div")]

Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

Array.from(arrayLike, x => x * x);
// 等同于
Array.from(arrayLike).map(x => x * x);

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

Array.from()能正确处理各种Unicode字符,因此可以将将字符串转为数组,然后正确返回字符串的长度。

function countSymbols(string) {
  return Array.from(string).length;
}
Array.of()

Array.of总是返回参数值组成的数组。如果没有参数,就返回一个空数组。Array.of基本上可以用来替代Array()或new Array()。

Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]

Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
数组的新增实例方法

copyWithin() :在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。Array.prototype.copyWithin(target, start = 0, end = this.length)。三个参数都应该是数值,如果不是,会自动转为数值。会修改当前数组。

find() :用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。可以发现NaN。

findIndex() :与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。可以发现NaN。

[NaN].indexOf(NaN)    // -1

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

//indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。

fill() :使用给定值,填充一个数组。fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。

["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"]  fill方法从1号位开始,向原数组填充7,到2号位之前结束。

includes():返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。该方法属于ES7,但Babel转码器已经支持。

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

[NaN].indexOf(NaN)  // -1
[NaN].includes(NaN) // true
//indexof会导致对NaN的误判,但是includes可以正确判断NaN。

遍历数组

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

for (let [index, elem] of ["a", "b"].entries()) {
  console.log(index, elem);
}
// 0 "a"
// 1 "b"

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

let letter = ["a", "b", "c"];
let entries = letter.entries();
console.log(entries.next().value); // [0, "a"]
console.log(entries.next().value); // [1, "b"]
数组的空位

数组的空位指,数组的某一个位置没有任何值。空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值。

Array(3) // [, , ,]     Array(3)返回一个具有3个空位的数组。
对空位的处理

ES5大多数情况下会忽略空位。

forEach(), filter(), every() 和some()都会跳过空位。

map()会跳过空位,但会保留这个值。

join()和toString()会将空位视为undefined,而undefined和null会被处理成空字符串。

// filter方法
["a",,"b"].filter(x => true) // ["a","b"]

// map方法
[,"a"].map(x => 1)  // [,1]

// join方法
[,"a",undefined,null].join("#") // "#a##"

ES6明确将空位转为undefined。

Array.from方法会将数组的空位,转为undefined。

扩展运算符(...)将空位转为undefined。

copyWithin()会连空位一起拷贝。

fill()会将空位视为正常的数组位置。

for...of循环也会遍历空位。

entries()、keys()、values()、find()和findIndex()会将空位处理成undefined。

Array.from(["a",,"b"])   // [ "a", undefined, "b" ]
[...["a",,"b"]]   // [ "a", undefined, "b" ]
new Array(3).fill("a") // ["a","a","a"]

由于空位的处理规则非常不统一,所以建议避免出现空位。

函数的扩展 函数参数的默认值

ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。参数变量是默认声明的,所以不能用let或const再次声明。使用参数默认值时,函数不能有同名参数。

//参数变量x是默认声明的,在函数体中,不能用let或const再次声明,否则会报错。
function foo(x = 5) {
  let x = 1; // error
  const x = 2; // error
}

如果参数默认值是变量,那么参数就不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。

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。
与解构赋值默认值结合使用

参数默认值可以与解构赋值的默认值结合起来使用。

function foo({x, y = 5}) {
  console.log(x, y);
}

foo({}) // undefined, 5
foo() // TypeError: Cannot read property "x" of undefined

只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值而生成。如果函数foo调用时参数不是对象,变量x和y就不会生成,从而报错。如果参数对象没有y属性,y的默认值5才会生效。

参数默认值的位置

通常情况下,定义了默认值的参数,应该是函数的尾参数。如果非尾部的参数设置默认值,则调用时无法只省略该参数,而不省略它后面的参数,除非显式输入undefined。

function f(x = 1, y) {
  return [x, y];
}
f(2) // [2, undefined])
f(, 1) // 报错
f(undefined, 1) // [1, 1]
函数的 length 属性

函数的length属性,将返回没有指定默认值的参数个数。设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数。rest参数也不会计入length属性。

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

一旦设置了参数的默认值,调用函数时,参数会形成一个多带带的作用域(context)。这种语法行为,在不设置参数默认值时,是不会出现的。当该多带带作用域里面默认值是变量,且变量未定义,则指向外层的全局变量,若此时该全局变量不存在,就会报错。

let x = 1;

function f(y = x) {
  let x = 2;
  console.log(y);
}

f() // 1

函数f调用时,参数y = x形成一个多带带的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的新声明局部变量x影响不到默认值变量x。

rest参数

rest 运算符:将一个不定数量的参数表示为一个数组。

ES6 引入 rest 参数(形式为“...变量名”),用于获取函数的多余参数,rest 参数中的变量代表一个数组。注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。函数的length属性,不包括 rest 参数。

function f(a, ...b) {
  console.log(b);
}
f(2,3,4,5)  //[3, 4, 5]

// 报错
function f(a, ...b, c) {
  // ...
}
扩展运算符

扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
在某种程度上,rest运算符和Spread运算符(即扩展运算符)相反,Spread运算符会“展开”元素使其变成多个元素,rest运算符会收集多个元素和“压缩”成一个单一的元素。

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

// ES6的写法
Math.max(...[14, 3, 77])

// 等同于
Math.max(14, 3, 77);
扩展运算符的应用

合并数组

// ES5
[1, 2].concat(more)
// ES6
[1, 2, ...more]

将字符串转为真正的数组。

[..."hello"]
// [ "h", "e", "l", "l", "o" ]

该写法能够正确识别32位的Unicode字符。

"xuD83DuDE80y".length // 4
[..."xuD83DuDE80y"].length // 3
//JavaScript会将32位Unicode字符,识别为2个字符,采用扩展运算符就没有这个问题。

扩展运算符内部调用的是数据结构的Iterator接口。所以任何Iterator接口的对象,都可以用扩展运算符转为真正的数组。对于那些没有部署Iterator接口的类似数组的对象,扩展运算符就无法将其转为真正的数组。

严格模式

从ES5开始,函数内部可以设定为严格模式。

function doSomething(a, b) {
  "use strict";
  // code
}

《ECMAScript 2016标准》规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

规定的原因:函数内部的严格模式,同时适用于函数体代码和函数参数代码。但是,函数执行的时候,先执行函数参数代码,然后再执行函数体代码。这样就有一个不合理的地方,只有从函数体代码之中,才能知道参数代码是否应该以严格模式执行,但是参数代码却应该先于函数体代码执行。因此,标准如此定义。

name 属性

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

var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"


const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"

Function构造函数返回的函数实例,name属性的值为anonymous。bind返回的函数,name属性值会加上bound前缀。

(new Function).name // "anonymous"
function foo() {};
foo.bind({}).name // "bound foo"
箭头函数

ES6允许使用“箭头”(=>)定义函数。如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来。由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号。箭头函数可以嵌套。

var f = v => v;
//等同于
var f = function(v) {
  return v;
};

var f = () => 5;
// 等同于
var f = function () { return 5 };

var getTempItem = id => ({ id: id, name: "Temp" });

箭头函数使用注意点:

函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。this指向的固定化,是因为箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替。

不可以使用yield命令,因此箭头函数不能用作Generator函数。

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

// ES5
function foo() {
  var _this = this;

  setTimeout(function () {
    console.log("id:", _this.id);
  }, 100);
}

上面代码中,转换后的ES5版本清楚地说明了,箭头函数里面根本没有自己的this,而是引用外层的this。

除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。

(function() {
  return [
    (() => this.x).bind({ x: "inner" })()
  ];
}).call({ x: "outer" });
// ["outer"]
//上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this。
“函数绑定”(function bind)运算符

ES7提出了“函数绑定”(function bind)运算符,用来取代call、apply、bind调用。虽然该语法还是ES7的一个提案,但是Babel转码器已经支持。

函数绑定运算符是并排的两个双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。双冒号运算符返回的还是原对象,因此可以采用链式写法。

foo::bar(...arguments);
// 等同于
bar.apply(foo, arguments);

var method = obj::obj.foo;
// 等同于
var method = ::obj.foo;
尾调用优化

尾调用就是指某个函数运行的最后一步是调用另一个函数。尾调用不一定出现在函数尾部,只要是最后一步操作即可。

function f(x){
  return g(x);
}
//函数f的最后一步是调用函数g,这就叫尾调用。


function f(x){
  return g(x) + 1;
}
//函数调用之后还有操作,不是尾调用。
function f(x){
  g(x);
}
//上面的函数等同于下面的代码,因此也不是尾调用。
//function f(x){
//  g(x);
//  return undefined;
//}
尾调用优化

函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

function f() {
  let m = 1;
  let n = 2;
  return g(m + n);
}
f();

// 等同于
function f() {
  return g(3);
}
f();

// 等同于
g(3);

上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除 f(x) 的调用帧,只保留 g(3) 的调用帧。

这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

尾递归

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120  计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 
//改写为尾递归
function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120 改写成了尾递归,只保留一个调用记录,复杂度 O(1) 。

由此可见,“尾调用优化”对递归操作意义重大。ES6明确规定,所有ECMAScript的实现,都必须部署“尾调用优化”。这就是说,在ES6中,只要使用尾递归,就不会发生栈溢出,相对节省内存。

递归函数的改写

尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。

严格模式

ES6的尾调用优化只在严格模式下开启,正常模式是无效的。

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

func.arguments:返回调用时函数的参数。

func.caller:返回调用当前函数的那个函数。

尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

function restricted() {
  "use strict";
  restricted.caller;    // 报错
  restricted.arguments; // 报错
}
restricted();
尾递归优化的实现

正常模式下,或者那些不支持该功能的环境中,采用“循环”换掉“递归”,以减少调用栈。

//正常递归函数
function sum(x, y) {
  if (y > 0) {
    return sum(x + 1, y - 1);
  } else {
    return x;
  }
}

sum(1, 100000)
//

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

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

上面就是蹦床函数的一个实现,它接受一个函数f作为参数。只要f执行后返回一个函数,就继续执行。这里是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题。

然后,要做的就是将原来的递归函数,改写为每一步返回另一个函数。

function sum(x, y) {
  if (y > 0) {
    return sum.bind(null, x + 1, y - 1);
  } else {
    return x;
  }
}
//sum函数的每次执行,都会返回自身的另一个版本
trampoline(sum(1, 100000));//然后,用蹦床函数执行sum,就不会发生调用栈溢出。
函数参数的尾逗号

ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。

对象的扩展 属性的简洁表示法

ES6 允许直接写入变量和函数,作为对象的属性和方法。简洁写法的属性名总是字符串。

var foo = "bar";
var baz = {foo};
baz // {foo: "bar"}
// 等同于
var baz = {foo: foo};
//ES6 允许在对象之中,直接写变量。这时,属性名为变量名, 属性值为变量的值。

var o = {
  method() {
    return "Hello!";
  }
};
// 等同于
var o = {
  method: function() {
    return "Hello!";
  }
};
属性名表达式

JavaScript语言定义对象的属性,有两种方法。方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内

// 方法一
obj.foo = true;

// 方法二
obj["a" + "bc"] = 123;

如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性。但ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。表达式还可以用于定义方法名。

let propKey = "foo";

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

注意,属性名表达式与简洁表示法,不能同时使用,会报错。

// 报错
var foo = "bar";
var bar = "abc";
var baz = { [foo] };

// 正确
var foo = "bar";
var baz = { [foo]: "abc"};

注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object],这一点要特别小心。

const keyA = {a: 1};
const keyB = {b: 2};

const myObject = {
  [keyA]: "valueA",
  [keyB]: "valueB"
};

myObject // Object {[object Object]: "valueB"}
//[keyA]和[keyB]得到的都是[object Object],所以[keyB]会把[keyA]覆盖掉,而myObject最后只有一个[object Object]属性。
方法的 name 属性

对象方法的name属性返回函数名(即方法名)。如果对象的方法使用了取值函数(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。

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

Object.is()

ES5使用相等运算符(==)和严格相等运算符(===)比较两个值是否相等。前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。ES6中可用Object.is来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。但是用Object.is比较时,+0不等于-0,NaN等于自身。

Object.is("foo", "foo")
// true
Object.is({}, {})
// false


+0 === -0 //true
NaN === NaN // false

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

Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

var target = { a: 1, b: 1 };

var source1 = { b: 2, c: 2 };
var source2 = { c: 3 };

Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}

如果只有一个参数,Object.assign会直接返回该参数。如果该参数不是对象,则会先转成对象,然后返回。由于undefined和null无法转成对象,所以如果它们作为参数,就会报错。若非对象参数出现在源对象的位置(即非首参数),则这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefined和null不在首参数,就不会报错。

Object.assign(null) // 报错

let obj = {a: 1};
Object.assign(obj, null) === obj // true

Object.assign不会拷贝对象的内部属性[[PrimitiveValue]]。布尔值、数值、字符串分别转成对应的包装对象时,它们的原始值都在包装对象的内部属性[[PrimitiveValue]]上面。只有字符串的包装对象,会产生可枚举属性,这些属性会被拷贝。因此其他类型的值(即数值、字符串和布尔值)不在首参数,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。

var v1 = "abc";
var v2 = true;
var v3 = 10;

var obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }

Object(true) // {[[PrimitiveValue]]: true}
Object(10)  //  {[[PrimitiveValue]]: 10}
Object("abc") // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}

Object.assign拷贝的属性是有限制的。

只拷贝源对象的自身属性和属性名为Symbol值的属性。

不拷贝继承属性。

不拷贝不可枚举的属性(enumerable: false)。

注意点

1.Object.assign方法实行的是浅拷贝,而不是深拷贝。 也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

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

obj1.a.b = 2;
obj2.a.b // 2
//Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

2.对于嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。

var target = { a: { b: "c", d: "e" } }
var source = { a: { b: "hello" } }
Object.assign(target, source)
// { a: { b: "hello" } }
//target对象的a属性被source对象的a属性整个替换掉

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

Object.assign([1, 2, 3], [4, 5])
// [4, 5, 3]
//Object.assign把数组视为属性名为0、1、2的对象,因此源数组的0号属性4覆盖了目标数组的0号属性1。
各类型值转换为对象

布尔值、数值、字符串分别转成对应的包装对象时,它们的原始值都在包装对象的内部属性[[PrimitiveValue]]上面。只有字符串的包装对象,会产生可枚举属性。 null或undefined转换为对象时将创建并返回一个空对象。

Object(1)
// Number {[[PrimitiveValue]]: 1}
Object("foo")
// String {0: "f", 1: "o", 2: "o", length: 3, [[PrimitiveValue]]: "foo"}
Object(null)
// Object {}
属性的可枚举性

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

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

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

for...in循环:只遍历对象自身的和继承的可枚举的属性

Object.keys():返回对象自身的所有可枚举的属性的键名

JSON.stringify():只串行化对象自身的可枚举的属性

Object.assign():会忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。
以上四个操作之中,最后一个是ES6新增的。只有for...in会返回继承的属性。

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

属性的遍历

for...in:循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)。

Object.keys(obj):返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)。

Object.getOwnPropertyNames(obj):返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性)。

Object.getOwnPropertySymbols(obj):返回一个数组,包含对象自身的所有Symbol属性。

Reflect.ownKeys(obj):返回一个数组,包含对象自身的所有属性,不管属性名是Symbol或字符串,也不管是否可枚举。

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

首先遍历所有属性名为数值的属性,按照数字排序。

其次遍历所有属性名为字符串的属性,按照生成时间排序。

最后遍历所有属性名为Symbol值的属性,按照生成时间排序。

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

__proto__属性(前后各两个下划线),用来读取或设置当前对象的prototype对象。目前,所有浏览器(包括 IE11)都部署了这个属性。该属性最好不要用。在实现上,__proto__调用的是Object.prototype.__proto__。

Object.setPrototypeOf()

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

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

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

如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果。由于undefined和null无法转为对象,所以如果第一个参数是undefined或null,就会报错。

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

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

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

function Rectangle() {
  // ...
}

var rec = new Rectangle();
Object.getPrototypeOf(rec) === Rectangle.prototype
// true

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

// 等同于 Object.getPrototypeOf(Number(1))
Object.getPrototypeOf(1)
// Number {[[PrimitiveValue]]: 0}
Object.getPrototypeOf(1) === Number.prototype // true
Object.getPrototypeOf(null)
// TypeError: Cannot convert undefined or null to object
Object.keys(),Object.values(),Object.entries()

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

Object.values():返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。ES2017 引入。返回数组的成员顺序,与《属性的遍历》部分介绍的排列规则一致。Object.values会过滤属性名为 Symbol 值的属性。如果参数不是对象,Object.values会先将其转为对象。

Object.values({ [Symbol()]: 123, foo: "abc" });  // ["abc"]
Object.values("foo")   // ["f", "o", "o"]
Object.values(42) // []
Object.values(true) // []

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

Object.entries():返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。该方法的行为与Object.values基本一致。原对象的属性名是一个 Symbol 值,该属性会被忽略。可将对象转为真正的Map结构。

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

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

对象的扩展运算符

ES2017 将扩展运算符(...)引入了对象。

(1)解构赋值
对象的解构赋值用于从一个对象取值,相当于将所有可遍历的、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。

解构赋值要求等号右边是一个对象,所以如果等号右边是undefined或null,就会报错,因为它们无法转为对象。

解构赋值必须是最后一个参数,否则会报错。

解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。

解构赋值不会拷贝继承自原型对象的属性。

let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
x // 1
y // 2
z // { a: 3, b: 4 }
//变量z是解构赋值所在的对象。它获取等号右边的所有尚未读取的键(a和b),将它们连同值一起拷贝过来。



let { x, y, ...z } = null; // 运行时错误
let { ...x, y, z } = { x: 1, y: 2, a: 3, b: 4 }; // 句法错误
var o = Object.create({ x: 1, y: 2 });
o.z = 3;
let { x, ...{ y, z } } = o;
x // 1
y // undefined
z // 3
//变量x是单纯的解构赋值,所以可以读取对象o继承的属性;变量y和z是双重解构赋值,只能读取对象o自身的属性,所以只有变量z可以赋值成功。

(2)扩展运算符
扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。这等同于使用Object.assign方法。扩展运算符可以用于合并两个对象。如果扩展运算符的参数是null或undefined,这个两个值会被忽略,不会报错。

let z = { a: 3, b: 4 };
let n = { ...z };
n // { a: 3, b: 4 }

let ab = { ...a, ...b };
// 等同于
let ab = Object.assign({}, a, b);


let emptyObject = { ...null, ...undefined }; // 不报错

如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。

let aWithOverrides = { ...a, x: 1, y: 2 };
// 等同于
let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
// 等同于
let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });
//a对象的x属性和y属性,拷贝到新对象后会被覆盖掉。

作用:可用来修改现有对象的部分属性。

let newVersion = {
  ...previousVersion,
  name: "New Name" // Override the name property
};
//newVersion对象自定义了name属性,其他属性全部复制自previousVersion对象。
Object.getOwnPropertyDescriptors()

Object.getOwnPropertyDescriptor:返回某个对象属性的描述对象(descriptor)。ES5引入。

 var obj = { p: "a" };

Object.getOwnPropertyDescriptor(obj, "p")
// Object { value: "a",
//   writable: true,
//   enumerable: true,
//   configurable: true
// }

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

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

Object.getOwnPropertyDescriptors(obj)
// { foo:
//    { value: 123,
//      writable: true,
//      enumerable: true,
//      configurable: true },
//   bar:
//    { get: [Function: bar],
//      set: undefined,
//      enumerable: true,
//      configurable: true } }
//Object.getOwnPropertyDescriptors方法返回一个对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象。

主要是为了解决Object.assign()无法正确拷贝get属性和set属性的问题,因为Object.assign方法总是拷贝一个属性的值,而不会拷贝它背后的赋值方法或取值方法。Object.getOwnPropertyDescriptors配合Object.create方法,将对象属性克隆到一个新对象时,属于浅拷贝。

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 }

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

Null 传导运算符

?.称为Null 传导运算符,该运算符若返回null或undefined,就不再往下运算,而是返回undefined。
Null 传导运算符有四种用法。

obj?.prop // 读取对象属性

obj?.[expr] // 同上

func?.(...args) // 函数或对象方法的调用

new C?.(...args) // 构造函数的调用

const firstName = (message
  && message.body
  && message.body.user
  && message.body.user.firstName) || "default";
//等同于
const firstName = message?.body?.user?.firstName || "default";

参考自:ECMAScript 6 入门

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/82663.html

相关文章

  • es6学习笔记--字符串的扩展、数组的扩展、对象的扩展

    摘要:字符串的扩展字符串的遍历器接口字符串可以被循环遍历。即能识别编号大于查询字符串是否包含某个字符返回布尔值,表示是否找到了参数字符串。返回布尔值,表示参数字符串是否在原字符串的头部。 字符串的扩展 1.字符串的遍历器接口 字符串可以被for...of循环遍历。 与es5的比较for循环虽可以遍历字符串,但不能识别大于oxFFFF的编码; 2.位置 --> 字符/码点 根据指定位置返回对应...

    不知名网友 评论0 收藏0
  • es6学习笔记-数值的扩展_V1.0_byKL

    摘要:学习笔记数值的扩展有一些不常用或者还不支持的就没有记录了总体来说本篇只是一个备忘而已用来检查一个数值是否为有限的。两个新方法只对数值有效,非数值一律返回。参考引用数值扩展 es6学习笔记-数值的扩展 有一些不常用或者还不支持的就没有记录了,总体来说本篇只是一个备忘而已 Number.isFinite(), Number.isNaN() Number.isFinite()用来检查一个数值...

    宋华 评论0 收藏0
  • es6学习笔记-函数扩展_v1.0_byKL

    摘要:学习笔记函数扩展函数参数的默认值如果参数默认值是变量,那么参数就不是传值的,而是每次都重新计算默认值表达式的值。属性函数的属性,返回该函数的函数名。箭头函数详细链接参考引用函数扩展 es6学习笔记-函数扩展_v1.0 函数参数的默认值 function Point(x = 0, y = 0) { this.x = x; this.y = y; } var p = ne...

    yuanzhanghu 评论0 收藏0
  • es6学习笔记-字符串的扩展_v1.0_byKL

    摘要:学习笔记字符串的扩展字符的表示法允许使用的形式表示一个字符,但在之前,单个码点仅支持到,超出该范围的必须用双字节形式表示,否则会解析错误。返回布尔值,表示参数字符串是否在源字符串的头部。,是引入了字符串补全长度的功能。 es6学习笔记-字符串的扩展_v1.0 字符的Unicode表示法 JavaScript 允许使用uxxxx的形式表示一个字符,但在 ES6 之前,单个码点仅支持u00...

    JaysonWang 评论0 收藏0
  • babel学习笔记

    摘要:经过一番折腾,总算是把自己项目里的配置调整好了,所有文件从原来的缩小到。折腾了不少时间,改动其实就一个地方,就是配置文件,记录一下自己折腾的过程。本以为那这两种方式取其一就行了。这感觉和想象中的不一样啊,说好的一个搞定一切的呢。。。 先是看到前端早读课【第1065期】再见,babel-preset-2015,听说现在有了babel-preset-env,别的什么preset都不需要了,...

    Aomine 评论0 收藏0

发表评论

0条评论

Zoom

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<