let和const
let
声明的变量只在它所在的代码块有效。for (let i = 0; i < 10; i++) { // ... } console.log(i); // ReferenceError: i is not defined
对于var,变量
i
是var
命令声明的,在全局范围内都有效,所以全局只有一个变量i
。每一次循环,变量i
的值都会发生改变,而循环内被赋给数组a
的函数内部的console.log(i)
,里面的i
指向的就是全局的i
。也就是说,所有数组a
的成员里面的i
,指向的都是同一个i
,导致运行时输出的是最后一轮的i
的值,也就是 10。对于let,如果使用
let
,声明的变量仅在块级作用域内有效,最后输出的是 6。var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 10 var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6
另外,
for
循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。for (let i = 0; i < 3; i++) { let i = 'abc'; console.log(i); } // abc // abc // abc
上面代码正确运行,输出了 3 次
abc
。这表明函数内部的变量i
与循环变量i
不在同一个作用域,有各自单独的作用域(同一个作用域不可使用let
重复声明同一个变量)。对于let变量必须先声明后使用,否则会报错。不存在变量提升。
ES6 明确规定,如果区块中存在
let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。(“暂时性死区”)var tmp = 123; if (true) { tmp = 'abc'; // ReferenceError let tmp; }
不允许重复声明,var允许重复定义,但是let和const是不允许的。
// 报错 function func() { let a = 10; var a = 1; } // 报错 function func() { let a = 10; let a = 1; }
块级作用域
ES5 只有全局作用域和函数作用域,没有块级作用域,ES6引入了块级作用域的概念。大括号包裹的就是块级作用域。
没有块级作用域引发的后果:
1. 内层变量可能会覆盖外层变量。
var tmp = new Date(); function f() { console.log(tmp); if (false) { var tmp = 'hello world'; } } f(); // undefined
内层变量tmp被提升到了函数开始位置,这个时候相当于:var tmp; console.log(tmp);
2. 用来计数的循环变量泄露为全局变量
var s = 'hello'; for (var i = 0; i < s.length; i++) { console.log(s[i]); } console.log(i); // 5
有了块级作用域,上层变量就不会被下层影响。
function f1() { let n = 5; if (true) { let n = 10; } console.log(n); // 5 }
有了块级作用域,IIFE写法也就被淘汰了。
// IIFE 写法 (function () { var tmp = ...; ... }()); // 块级作用域写法 { let tmp = ...; ... }
ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
// 第一种写法,报错 if (true) let x = 1; // 第二种写法,不报错 if (true) { let x = 1; }
考虑到环境导致的行为差异太大(不同环境可能会将function当成let或者var去处理),应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
// 块级作用域内部的函数声明语句,建议不要使用 { let a = 'secret'; function f() { return a; } } // 块级作用域内部,优先使用函数表达式 { let a = 'secret'; let f = function () { return a; }; }
Const
const
实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。const foo = {}; // 为 foo 添加一个属性,可以成功 foo.prop = 123; foo.prop // 123 // 将 foo 指向另一个对象,就会报错 foo = {}; // TypeError: "foo" is read-only
顶层对象
顶层对象,在浏览器环境指的是
window
对象,在 Node 指的是global
对象。ES5 之中,顶层对象的属性与全局变量是等价的。window.a = 1; a // 1 a = 2; window.a // 2
ES6 为了改变这一点,一方面规定,为了保持兼容性,
var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。
- 浏览器里面,顶层对象是
window
,但 Node 和 Web Worker 没有window
。- 浏览器和 Web Worker 里面,
self
也指向顶层对象,但是 Node 没有self
。- Node 里面,顶层对象是
global
,但其他环境都不支持以下是常用的两种方式获取顶层对象。
// 方法一 (typeof window !== 'undefined' ? window : (typeof process === 'object' && typeof require === 'function' && typeof global === 'object') ? global : this); // 方法二 var getGlobal = function () { if (typeof self !== 'undefined') { return self; } if (typeof window !== 'undefined') { return window; } if (typeof global !== 'undefined') { return global; } throw new Error('unable to locate global object'); };
数据解构
事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。
let [foo, [[bar], baz]] = [1, [[2], 3]]; foo // 1 bar // 2 baz // 3 let [ , , third] = ["foo", "bar", "baz"]; third // "baz" let [x, , y] = [1, 2, 3]; x // 1 y // 3 let [head, ...tail] = [1, 2, 3, 4]; head // 1 tail // [2, 3, 4] let [x, y, ...z] = ['a']; x // "a" y // undefined z // [] let [x, y] = [1, 2, 3]; x // 1 y // 2 let [a, [b], d] = [1, [2, 3], 4]; a // 1 b // 2 d // 4 // 报错 let [foo] = 1; let [foo] = false; let [foo] = NaN; let [foo] = undefined; let [foo] = null; let [foo] = {}; let [x, y, z] = new Set(['a', 'b', 'c']); x // "a" //fibs是一个 Generator 函数 function* fibs() { let a = 0; let b = 1; while (true) { yield a; [a, b] = [b, a + b]; } } let [first, second, third, fourth, fifth, sixth] = fibs(); sixth // 5 // 解构赋值允许指定默认值,只有当一个数组成员严格等于undefined,默认值才会生效 let [x, y = 'b'] = ['a']; // x='a', y='b' let [x, y = 'b'] = ['a', undefined]; // x='a', y='b' let [x = 1] = [null]; // x = null // 默认值可以引用解构赋值的其他变量,但该变量必须已经声明。 let [x = 1, y = x] = []; // x=1; y=1 let [x = 1, y = x] = [2]; // x=2; y=2 let [x = 1, y = x] = [1, 2]; // x=1; y=2 let [x = y, y = 1] = []; // ReferenceError: y is not defined // 可以解构对象 let { bar, foo } = { foo: 'aaa', bar: 'bbb' }; // foo = "aaa", bar = "bbb" let {foo} = {bar: 'baz'}; // foo = undefined // 这实际上说明,对象的解构赋值是下面形式的简写 let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' }; let { foo: fooA, bar: barA } = { foo: 'aaa', bar: 'bbb' }; // fooA = 'aaa', barA = 'bbb' var {x: y = 3} = {}; // y = 3 // 嵌套类型的解构 let obj = { p: [ 'Hello', { y: 'World' } ] }; //数组本质是特殊的对象,因此可以对数组进行对象属性的解构 let arr = [1, 2, 3]; let {0 : first, [arr.length - 1] : last} = arr; first // 1 last // 3 // 此时p是模式,不是变量 let { p: [x, { y }] } = obj; x // "Hello" y // "World" // 下面方式可以获得p let { p, p: [x, { y }] } = obj; x // "Hello" y // "World" p // ["Hello", {y: "World"}] // 嵌套赋值 let obj = {}; let arr = []; ({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true }); obj // {prop:123} arr // [true] // 对象的解构赋值可以取到继承的属性。 const obj1 = {}; const obj2 = { foo: 'bar' }; Object.setPrototypeOf(obj1, obj2); const { foo } = obj1; foo // "bar" // 上面代码中,对象obj1的原型对象是obj2。foo属性不是obj1自身的属性,而是继承自obj2的属性,解构赋值可以取到这个属性。 //数组本质是特殊的对象,因此可以对数组进行对象属性的解构 let arr = [1, 2, 3]; let {0 : first, [arr.length - 1] : last} = arr; first // 1 last // 3 // 字符串的解构赋值 // 字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。 const [a, b, c, d, e] = 'hello'; a // "h" b // "e" c // "l" d // "l" e // "o" 类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。 let {length : len} = 'hello'; len // 5 // 函数解构 function add([x, y]){ return x + y; } add([1, 2]); // 3 // 变量交换 let x = 1; let y = 2; [x, y] = [y, x]; // x= 2, y = 1
遍历Map结构
任何部署了 Iterator 接口的对象,都可以用
for...of
循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。const map = new Map(); map.set('first', 'hello'); map.set('second', 'world'); for (let [key, value] of map) { console.log(key + " is " + value); }
字符串扩展
支持字符的 Unicode 表示法
"\u0061" // "a" "\uD842\uDFB7" // "𠮷" "\u20BB7" // " 7" "\u{20BB7}" // "𠮷" "\u{41}\u{42}\u{43}" // "ABC"
ES6 为字符串添加了遍历器接口Iterator,可被for/of
支持字符串模板
`foo ${fn()} bar` let obj = {x: 1, y: 2}; `${obj.x + obj.y}`
模板标签
alert`hello` // 等同于 alert(['hello']) // 但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。 let a = 5; let b = 10; tag`Hello ${ a + b } world ${ a * b }`; // 等同于 tag(['Hello ', ' world ', ''], 15, 50);
字符串的新增方法
正则扩展
包括:
- RegExp 构造函数
- 字符串的正则方法
- u 修饰符
- RegExp.prototype.unicode 属性
- y 修饰符
- RegExp.prototype.sticky 属性
- RegExp.prototype.flags 属性
- s 修饰符:dotAll 模式
- 后行断言
- Unicode 属性类
- 具名组匹配
- 正则匹配索引
- String.prototype.matchAll()
ES6 出现之前,字符串对象共有 4 个方法,可以使用正则表达式:
match()
、replace()
、search()
和split()
。ES6 将这 4 个方法,在语言内部全部调用
RegExp
的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp
对象上。
String.prototype.match
调用RegExp.prototype[Symbol.match]
String.prototype.replace
调用RegExp.prototype[Symbol.replace]
String.prototype.search
调用RegExp.prototype[Symbol.search]
String.prototype.split
调用RegExp.prototype[Symbol.split]
// 解构 let {groups: {one, two}} = /^(?<one>.*):(?<two>.*)$/u.exec('foo:bar'); one // foo two // bar // 替换 let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u; '2015-01-02'.replace(re, '$<day>/$<month>/$<year>')
数值的扩展
- 二进制和八进制表示法
- 数值分隔符
- Number.isFinite(), Number.isNaN()
- Number.parseInt(), Number.parseFloat()
- Number.isInteger()
- Number.EPSILON
- 安全整数和 Number.isSafeInteger()
- Math 对象的扩展
- BigInt 数据类型
ES6允许_作为数字分隔符,无实际意义:
1_2_3 === 123 // true
函数的扩展
非尾部参数无法省略
function f(x = 1, y) { return [x, y]; } f() // [1, undefined] f(2) // [2, undefined] f(, 1) // 报错
函数的 length 属性
指定了默认值以后,函数的
length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。(function (a) {}).length // 1 (function (a = 5) {}).length // 0 (function (a, b, c = 5) {}).length // 2 (function(...args) {}).length // 0 // 如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。 (function (a = 0, b, c) {}).length // 0 (function (a, b = 1, c) {}).length // 1
箭头函数
function Timer() { this.s1 = 0; this.s2 = 0; // 箭头函数 setInterval(() => { console.log("first:" + this.s1) //this.s1 = 0 this.s1++ }, 1000); // 普通函数 setInterval(function () { console.log("second:" + this.s2) // this.s1 = undefined this.s2++; }, 1000); } var timer = new Timer(); setTimeout(() => console.log('s1: ', timer.s1), 3100); setTimeout(() => console.log('s2: ', timer.s2), 3100); // s1: 3 // s2: 0
Timer
函数内部设置了两个定时器,分别使用了箭头函数和普通函数。箭头函数的this
绑定定义时所在的作用域(即Timer
函数),普通函数的this
指向运行时所在的作用域(即全局对象)总之,箭头函数根本没有自己的
this
,导致内部的this
就是外层代码块的this
。正是因为它没有this
,所以也就不能用作构造函数。下面ES6转ES5代码:
// ES6 function foo() { setTimeout(() => { console.log('id:', this.id); }, 100); } // ES5 function foo() { var _this = this; setTimeout(function () { console.log('id:', _this.id); }, 100); }
function foo() { return () => { return () => { return () => { console.log('id:', this.id); }; }; }; } this总是指向foo的this,不管怎么调用,都是最外层对象 var f = foo.call({id: 1}); var t1 = f.call({id: 2})()(); // id: 1 var t2 = f().call({id: 3})(); // id: 1 var t3 = f()().call({id: 4}); // id: 1 // 另外,由于箭头函数没有自己的this,所以当然也就不能用call()、apply()、bind()这些方法去改变this的指向。下面的列子,this总是指向最外层,无论给它绑定哪个对象。 (function() { return [ (() => this.x).bind({ x: 'inner' })() ]; }).call({ x: 'outer' }); // ['outer'] // 除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:arguments、super、new.target。 function foo() { setTimeout(() => { console.log('args:', arguments); }, 100); } foo(2, 4, 6, 8) // args: [2, 4, 6, 8] // 上面代码中,箭头函数内部的变量arguments,其实是函数foo的arguments变量。
不适合使用箭头函数的场景
const cat = { lives: 9, jumps: () => { this.lives--; } }
如果写成上面那样的箭头函数,使得
this
指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致jumps
箭头函数定义时的作用域就是全局作用域。如果是普通函数,该方法内部的this
指向cat
;所以建议: 对象的属性建议使用传统的写法定义,不要用箭头函数定义。第二个场合是需要动态
this
的时候,也不应使用箭头函数。var button = document.getElementById('press'); button.addEventListener('click', () => { this.classList.toggle('on'); });
上面代码运行时,点击按钮会报错,因为
button
的监听函数是一个箭头函数,导致里面的this
就是全局对象。如果改成普通函数,this
就会动态指向被点击的按钮对象。尾调用优化
尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。
// 是尾调用 function f(x){ return g(x); } // m,n都是尾调用 function f(x) { if (x > 0) { return m(x) } return n(x); } 以下三种情况,都不属于尾调用。 // 情况一 function f(x){ let y = g(x); return y; } // 情况二 function f(x){ return g(x) + 1; 或 return g(x) + q(x); } // 情况三 function f(x){ g(x); }
非尾调用会形成很长的调用栈,把下一个调用的信息也存起来,很容易造成内存溢出。
对于递归调用,很耗内存,我们一般需要进行“尾调用优化”,这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。
function factorial(n, total = 1) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5) // 120
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
func.arguments
:返回调用时函数的参数。func.caller
:返回调用当前函数的那个函数。尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
数组的扩展
- 扩展运算符
- Array.from()
- Array.of()
- 实例方法:copyWithin()
- 实例方法:find() 和 findIndex()
- 实例方法:fill()
- 实例方法:entries(),keys() 和 values()
- 实例方法:includes()
- 实例方法:flat(),flatMap()
- 实例方法:at()
- 数组的空位
- Array.prototype.sort() 的排序稳定性
// 数组的扩展和解构 const arr1 = ['a', 'b']; const arr2 = ['c']; [...arr1, ...arr2] // [ 'a', 'b', 'c' ] [a, ...rest] = [1,2] // a = 1 // rest =[2] // 字符解构 [...'hello'] // [ "h", "e", "l", "l", "o" ] 'x\uD83D\uDE80y'.length // 4 四个字节 [...'x\uD83D\uDE80y'].length // 3 数组长度为3 [ 'x', '🚀', 'y' ]
任何定义了遍历器(Iterator)接口的对象,都可以用扩展运算符转为真正的数组。
querySelectorAll
方法返回的是一个NodeList
对象就实现了 Iterator。Arrary.from,可以将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)
Array.from(new Set(['a', 'b'])) // ['a', 'b'] Array.from('hello')// ['h', 'e', 'l', 'l', 'o'] Array.from([1, 2, 3]) //[1, 2, 3] // 类似数组的对象,本质特征只有一点,即必须有length属性 Array.from({0:"x",1:"y", length: 2})// [ 'x', 'y' ] // Array.of()方法用于将一组值,转换为数组,弥补Array构造函数的不足。 Array.of(3, 11, 8) // [3,11,8] Array.of(3) // [3] Array.of(3).length // 1 // 这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。 Array() // [] Array(3) // [, , ,] Array(3, 11, 8) // [3, 11, 8] // copyWithin()方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组,会修改当前数组。 // 将3号位开始,4号位结束的元素,替换到0号位开始 [1, 2, 3, 4, 5].copyWithin(0, 3, 4) // [4, 2, 3, 4, 5] // find和findIndex,用于查找元素,比较常用: [1, 4, -5, 10].find((n) => n < 0)// -5 返回找到的元素,否则undefined [1, 4, -5, 10].findIndex((n) => n < 0) //2 // 返回下标,否则-1 // 由于NaN === NaN // false [NaN].indexOf(NaN) // -1 [NaN].findIndex(y => Object.is(NaN, y))// 0 弥补了这个问题 // fill方法使用给定值,填充一个数组。 ['a', 'b', 'c'].fill(7) // [7, 7, 7] ['a', 'b', 'c'].fill(7, 1, 2) // ['a', 7, 'c'] 指定填充的起始位置和结束位置 // entries(),keys() 和 values(), 用于遍历数组。它们都返回一个遍历器对象,可以用for...of循环进行遍历 for (let index of ['a', 'b'].keys()) { console.log(index); } // 0 // 1 for (let value of ['a', 'b'].values()) { console.log(value); } // 'a' // 'b' for (let [index, value] of ['a', 'b'].entries()) { console.log(index, value); } // 0 "a" // 1 "b" // Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似 [1, 2, 3].includes(2) // true [1, 2, 3].includes(4) // false [1, 2, NaN].includes(NaN) // true // 数组的空位,数组的空位指的是,数组的某一个位置没有任何值,比如Array()构造函数返回的数组都是空位。空位不是undefined,是没值。 0 in [undefined, undefined, undefined] // true 0 in [, , ,] // false // forEach方法 [,'a'].forEach((x,i) => console.log(i)); // 1 // filter方法 ['a',,'b'].filter(x => true) // ['a','b'] // every方法 [,'a'].every(x => x==='a') // true // reduce方法 [1,,2].reduce((x,y) => x+y) // 3 // some方法 [,'a'].some(x => x !== 'a') // false // map方法 [,'a'].map(x => 1) // [,1] // join方法 [,'a',undefined,null].join('#') // "#a##" // toString方法 [,'a',undefined,null].toString() // ",a,," ES6 则是明确将空位转为undefined。 Array.from(['a',,'b']) // [ "a", undefined, "b" ] [...['a',,'b']] // [ "a", undefined, "b" ] entries()、keys()、values()、find()和findIndex()会将空位处理成undefined。 // entries() [...[,'a'].entries()] // [[0,undefined], [1,"a"]] // keys() [...[,'a'].keys()] // [0,1] // values() [...[,'a'].values()] // [undefined,"a"] // find() [,'a'].find(x => true) // undefined // findIndex() [,'a'].findIndex(x => true) // 0 // sort arr.sort((x,y)=> {return -1 or 1})
对象的扩展
// 允许简写对象 const foo = 'bar'; const baz = {foo}; // 等价 {foo: foo} // 表达式作为属性名,这时要将表达式放在方括号之内。 obj['a' + 'bc'] = 123; let propKey = 'foo'; let obj = { [propKey]: true, ['a' + 'bc']: 123 }; //属性名表达式与简洁表示法,不能同时使用,会报错 // 报错 const foo = 'bar'; const bar = 'abc'; const baz = { [foo] }; // super 关键字 // 我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。 // super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。 const proto = { foo: 'hello' }; const obj = { foo: 'world', find() { return super.foo; } }; Object.setPrototypeOf(obj, proto); obj.find() // "hello" // 报错 const obj = { foo: super.foo } // 报错 const obj = { foo: () => super.foo } // 报错 const obj = { foo: function () { return super.foo } }
对象的解构赋值
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; x // 1 y // 2 z // { a: 3, b: 4 }
Object新增方法
// Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。 Object.is('foo', 'foo') // true Object.is({}, {}) // false 不同之处只有两个:一是+0不等于-0,二是NaN等于自身。 +0 === -0 //true NaN === NaN // false Object.is(+0, -0) // false Object.is(NaN, NaN) // true // 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.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.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的原型对象(prototype),返回参数对象本身 // 格式 Object.setPrototypeOf(object, prototype) // 用法 const o = Object.setPrototypeOf({}, null); //该方法等同于下面的函数。 function setPrototypeOf(obj, proto) { obj.__proto__ = proto; return obj; } // Object.getPrototypeOf()该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。 Object.getPrototypeOf(obj); // Object.keys(),Object.values(),Object.entries()
运算符扩展
指数运算符
2 ** 2 // 4 2 ** 3 // 8
链判断运算符
// 在之前判空需要一层层的判断,很不方便. // ES2020 引入了“链判断运算符”(optional chaining operator)?.,简化上面的写法。写法如下: // 上面代码使用了?.运算符,直接在链式调用的时候判断,左侧的对象是否为null或undefined。 // 如果是的,就不再往下运算,而是返回undefined const firstName = message?.body?.user?.firstName || 'default';
链判断运算符
?.
有三种写法。
obj?.prop
// 对象属性是否存在obj?.[expr]
// 同上func?.(...args)
// 函数或对象方法是否存在短路机制 -
?.
运算符相当于一种短路机制,只要不满足条件,就不再往下执行括号的影响 - 只对圆括号内部有影响。
(a?.b).c // 等价于 (a == null ? undefined : a.b).c // 上面代码中,?.对圆括号外部没有影响,不管a对象是否存在,圆括号后面的.c总是会执行。
以下写法是禁止的,会报错。
// 构造函数 new a?.() new a?.b() // 链判断运算符的右侧有模板字符串 a?.`{b}` a?.b`{c}` // 链判断运算符的左侧是 super super?.() super?.foo // 链运算符用于赋值运算符左侧 a?.b = c
右侧不得为十进制数值
为了保证兼容以前的代码,允许
foo?.3:0
被解析成foo ? .3 : 0,因此规定如果?.后面紧跟一个
十进制数字,那么?.不再被看成是一个完整的运算符,而会按照三元运算符进行处理
Null 判断运算符
我们一般通过||
运算符指定默认值,ES2020 引入了一个新的 Null 判断运算符??
。它的行为类似||
,但是只有运算符左侧的值为null
或undefined
时,才会返回右侧的值。response.settings?.animationDuration ?? 300 // 上面代码中,如果response.settings是null或undefined, // 或者response.settings.animationDuration是null或undefined,就会返回默认值300。 // 也就是说,这一行代码包括了两级属性的判断。
逻辑赋值运算符
ES2021 引入了三个新的逻辑赋值运算符
// 或赋值运算符 x ||= y // 等同于 x || (x = y) // 与赋值运算符 x &&= y // 等同于 x && (x = y) // Null 赋值运算符 x ??= y // 等同于 x ?? (x = y) // 这三个运算符||=、&&=、??=相当于先进行逻辑运算,然后根据运算结果,再视情况进行赋值运算。 // 老的写法 user.id = user.id || 1; // 新的写法 user.id ||= 1; // 老的写法 function example(opts) { opts.foo = opts.foo ?? 'bar'; opts.baz ?? (opts.baz = 'qux'); } // 新的写法 function example(opts) { opts.foo ??= 'bar'; opts.baz ??= 'qux'; }
Symbol
ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入
Symbol
的原因。ES6 引入了一种新的原始数据类型
Symbol
,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined
、null
、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。// 没有参数的情况 let s1 = Symbol(); let s2 = Symbol(); s1 === s2 // false // 有参数的情况 let s1 = Symbol('foo'); let s2 = Symbol('foo'); s1 === s2 // false // Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的 s1.description // "foo" String(s1) // "Symbol(foo)" s1.toString() // "Symbol(foo)"
由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
let mySymbol = Symbol(); // 第一种写法 let a = {}; a[mySymbol] = 'Hello!'; // 第二种写法 let a = { [mySymbol]: 'Hello!' }; // 第三种写法 let a = {}; Object.defineProperty(a, mySymbol, { value: 'Hello!' }); // 以上写法都得到同样结果 a[mySymbol] // "Hello!"
Symbol.for(),Symbol.keyFor()
Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值。
let s1 = Symbol.for('foo'); let s2 = Symbol.for('foo'); s1 === s2 // true // 上面代码中,s1和s2都是 Symbol 值,但是它们都是由同样参数的Symbol.for方法生成的,所以实际上是同一个值。
let s1 = Symbol.for("foo"); Symbol.keyFor(s1) // "foo" let s2 = Symbol("foo"); Symbol.keyFor(s2) // undefined
Symbol.keyFor()
方法返回一个已登记的 Symbol 类型值的key
。