关于 ES6 / ES7
写在前面
0.1 JavaScript 语句的自动分号插入
一条语句以“(”、“[”、“/”、“+”或“-”开始,那么它极有可能和前一条语句合在一起解析。保守起见,可以在语句前加上一个分号以保证程序的正确执行;
var a = 0 // 这里省略了分号 ;[a, a+1, a+2].forEach(console.log) // 前面的分号保证了正确地语句解析
0.2 闭包( closure )
函数内部形成一个局部作用域,定义在这个局部作用域上的内部函数,会持有对这个原始作用域的引用,只要在外部作用域上执行这个内部函数并访问原始作用域,就会形成闭包。
判断是否是闭包,需满足两个条件:
(1) 一个词法环境A(可以简单理解为函数)中创建另一个新的词法环境B。
(2) B执行的过程中,调用了A中的变量。
0.3 this 判断优先级
(1) 函数在 new 中调用,this 绑定的是新创建的实例化对象。
var bar = new foo() // this 指向 bar
(2) 函数通过 call、apply 绑定调用(显式绑定),this 绑定的是指定的对象
var bar = foo.call(obj_other) // this 指向obj
(3)函数在某个上下文对象中调用(隐式绑定),this 绑定的是上下文对象
obj.foo() // this 指向 obj
(4)否则,属于默认绑定,this 绑定的是全局对象(严格模式下为undefined)
var bar = obj.foo; bar() // this 指向全局对象
0.4 new 运算符
假设有一个构造函数 Base,对其实例化一个对象 obj :
var obj = new Base()
new 关键字会进行如下的操作:
// 1. 创建一个对象 var obj={}; // 2. 将这个空对象 obj 的 __proto__ 指向了 Base 函数对象 prototype 成员对象 obj.__proto__ = Base.prototype; // 3. 将 Base 函数对象的 this 指针替换成 obj,然后再调用Base函数。即:改变构造函数的 this 指向,来给 obj 添加属性和方法 Base.call(obj);
1. async / await
async 确保了函数的返回值是一个 promise,也会包装非 promise 的值。
// 常规函数 function foo(){ return 123 } foo() // 123 // 加限定词 async async function foo(){ return 123 } foo() // Promise {<resolved>: 123} // 显式返回一个 promise async function foo(){ return Promise.resolve(123); } foo() // Promise {<resolved>: 123}
await 让 JavaScript 引擎等待直到 promise 完成 ( * ) 并返回结果 。
async function ff() { let promise = new Promise( resolve=> { setTimeout(() => resolve(456), 1000) }); let result = await promise; // 等待直到 promise 决议 并返回 resolve 的值 (*) console.log(result); // 456 } ff();
相比 promise.then 来获取 promise 结果,这只是一个更优雅的语法,同时也更可读和更易书写。
如果 promise 被拒绝(rejected),就会抛出一个错误,就像在那一行有个 throw 语句那样。可以用 try..catch 来捕获上面的错误,或者我们也可以在函数调用后添加 .catch 来处理错误:
// try... catch... async function f() { try { let response = await fetch('http://url'); } catch(err) { console.log(err); // TypeError: failed to fetch } } f(); // .catch async function f() { let response = await fetch('http://url'); } f().catch(console.log); // TypeError: failed to fetch
2. 箭头函数
函数体内的 this 对象,就是定义时所在的对象,而不是使用时所在的对象。这意味着 this 和 arguments 都是从它们的父函数继承而来的。如下:
const func = { name: 'example', anonFunction: function() { return function() { console.log('--------anon-------'); console.log(this); console.log(this.name); console.log(arguments); }; }, arrowFunction: function() { return () => { console.log('-------arrow--------'); console.log(this); console.log(this.name); console.log(arguments); }; } }; const anon = func.anonFunction('hello', 'world'); const arrow = func.arrowFunction('hello', 'world'); anon() // Window {parent: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …} // // Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ] anon('other') // Window {parent: Window, postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, …} // // Arguments ["other", callee: ƒ, Symbol(Symbol.iterator): ƒ] arrow() arrow('other') // {name: "example", anonFunction: ƒ, arrowFunction: ƒ} // example // Arguments(2) ["hello", "world", callee: ƒ, Symbol(Symbol.iterator): ƒ]
与其他形式的函数不同,箭头函数没有自己的执行期上下文。this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。
3. 解构赋值
3.1 数组解构
let [a, b, c] = [1, 2, 3]; // 从数组中提取值,按照对应位置,对变量赋值
/** 不完全匹配 **/
let [head, ...tail] = [1, 2, 3, 4];
head // 1
tail // [2, 3, 4]
let [x, , y] = [1, 2, 3];
x // 1
y // 3
/** 添加默认值 **/
let [i, j = 'b'] = ['a']; // i='a', j='b'
/** 交换值 **/
let [m, n] = [1, 2];
[m, n] = [n, m];
m // 2
n // 1
3.2 对象解构
对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。形式如下:
let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
对象的属性没有次序,变量必须与属性同名,才能取到正确的值。
let { bar, foo } = { foo: 'aaa', bar: 'bbb' }; foo // "aaa" bar // "bbb"
若想重写变量名
let { foo: baz } = { foo: 'aaa', bar: 'bbb' }; baz // "aaa"
可以用于多层嵌套结构的对象
let obj = { p: [ 'Hello', { y: 'World' } ] }; let { p: [x, { y }] } = obj; x // "Hello" y // "World"
可用于函数的默认值解构
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) { console.log(method); } fetch('http://example.com') // "GET"
4. 数组、对象 的扩展
4.1 新增数组方法
Array.of() 用于将一组值,转换为数组
Array.of(3, 11, 8) // [3,11,8] Array.of(3) // [3] Array.of(3).length // 1
entries(),keys() 和 values() 都返回一个遍历器对象,可用 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"
4.2 对象新增方法
Object.assign 用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target),并返回目标对象。第一个参数是目标对象,后面的参数都是源对象,后面的属性会覆盖前面的属性。实行的是浅拷贝
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.keys(),Object.values(),Object.entries() 同数组,keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历
4.3 扩展运算符
结构为( ... ),可以将一个数组转为用逗号分隔的参数序列,
console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5
也可作用于 Object
let z = { a: 3, b: 4 }; let n = { ...z }; n // { a: 3, b: 4 }
5. Promise
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
new Promise((resolve,reject)=>{ resolve(123) }).then((res)=>{ console.log(res); return res }).then((res)=>{ console.log(res) }) // 123 // 123 // 甚至可以这样 Promise.resolve()
.then(_=>{console.log('ok')}) .then(_=>{console.log('ok')}) .then(_=>{console.log('ok')}) .then(_=>{console.log('ok')}) .then(_=>{console.log('ok')}) .then(_=>{console.log('ok')})
.catch 方法是 .then(null, rejection) 或 .then(undefined, rejection) 的别名。Promise 对象后面的 catch 方法,可以处理 Promise 内部,以及 catch 前面的 then 方法内发生的错误。catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法。运行完catch方法指定的回调函数,会接着运行后面那个then方法指定的回调函数。如果没有报错,则会跳过catch方法。
Promise.resolve().catch((error)=> { console.log('catch:', error); }).then(()=> { console.log('carry on'); }); // carry on
finally 方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
6. Class
ES6 的 class 只是一个语法糖,它的绝大部分功能,ES5 都可以做到。本质是使用 es5 的构造函数来模拟 class 类。
// class 声明 class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; } } /** * 等同于: * * function Point(x, y) { * this.x = x; * this.y = y; * } * Point.prototype.toString = function () { * return '(' + this.x + ', ' + this.y + ')'; * }; * */
因此,以上代码中,可以直接通过原型链方式,给 Point 添加 类方法
class Point { // ... } Point.prototype.join = function () { console.log(this.x + '__' + this.y); }; let p = new Point(1,2) p.join() // 1__2
constructor 方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。默认返回实例对象(即this),也可以指定返回另外一个对象。
在类内部的方法前面,加上 static 关键字,表示该方法为静态方法,不会被实例继承,而是直接通过类来调用。
class Foo { static func() { return 'hello'; } } Foo.func() // 'hello' var foo = new Foo(); foo.func() // TypeError: foo.funcis not a function
可以通过 extends 关键字实现 class类 的继承。父类的静态方法,也会被子类继承。
class ColorPoint extends Point { constructor(x, y, color) { super(x, y); // 调用父类的constructor(x, y) this.color = color; } toString() { return this.color + ' ' + super.toString(); // 调用父类的toString() } }
子类自己的 this 对象,必须先通过父类的构造函数来构造,然后才能加上子类自己的实例属性和方法。super 关键字,表示父类的构造函数,用来新建父类的this对象。因此,子类必须在 constructor 方法中调用super方法,不调用super方法,子类就得不到this对象,新建实例时会报错。
class Parent {} class Child extends Parent { constructor(){} } new Child() // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor
需要注意的是,如果子类没有定义 constructor 方法,这个方法会被默认添加
class Parent {} class Child extends Parent { // ... } new Child() // 这段代码没有报错,因为以上操作等同于: class Parent {} class Child extends Parent { constructor(...args){ super(...args); } } new Child()
同样,子类的构造函数中,只有调用super之后,才可以使用 this 关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有 super 方法才能调用父类实例。
super 可用作函数调用,也可作为对象使用
(1)super 作为函数调用时,代表父类的构造函数,只能用在子类的构造函数之中。super 虽然代表了父类的构造函数,但是返回的是子类的实例;
(2)super 作为对象时,在普通方法中,指向父类的原型对象,但是无法调用父类实例上的方法或属性( 例如 this.xx 方式定义);在静态方法中,指向父类。
7. Proxy