参阅 阮一峰 ECMAScript 6 入门 ,作一个归纳整理吧。。。
ES6 : 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。
ES6 的第一个版本在 2015 年 6 月发布,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)
声明变量
- var (ES5)
- function (ES5)
- let
- const
- class
- import
var/let/const:
scope:
{ var a = 1; let b = 10; const c = 20; } a // 1 b // ReferenceError: b is not defined. c // ReferenceError: c is not defined.
var
: 声明的变量在全局范围内都有效;会发生”变量提升“现象(即变量可以在声明之前使用,值为undefined)var a = []; for (var i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 10
let
: 声明的变量只在所在的代码块内有效(局部有效);声明的变量一定要在声明后使用,否则报错(“暂时性死区”)var a = []; for (let i = 0; i < 10; i++) { a[i] = function () { console.log(i); }; } a[6](); // 6
const
: 声明一个只读的常量;与let相同,只在声明所在的块级作用域内有效,同样存在暂时性死区- ES5 常量写法:
Object.defineProperty(window,"PI",{ value:3.1415926, writable:false }); console.log(window.PI);
- ES6 常量写法(使用const)
const PI = 3.1415; PI // 3.1415 PI = 3; // TypeError: Assignment to constant variable.
- scope:
const foo; // SyntaxError: Missing initializer in const declaration if (true) { const MAX = 5; MAX // 5 } MAX // Uncaught ReferenceError: MAX is not defined
- ES5 常量写法:
注:
const
实际上保证的是变量指向的内存地址中保存的数据不得改动所以对于简单类型的数据(例如:数值、字符串、布尔值)可以保证只读,但对于复合类型的数据就无法保证了
const foo = {}; foo.prop = 123; // 成功 foo = {}; // TypeError: "foo" is read-only const a = []; a.push('Hello'); // 成功 a.length = 0; // 成功 a = ['Dave']; // 报错
- 在
let
和const
之间,建议优先使用const
,尤其是在全局环境,不应该设置变量,只应设置常量
块级作用域 {}
可代替闭包
- ES5 闭包(执行函数表达式 IIFE)写法:
(function () { var tmp = ...; ... }());
- ES6 块级作用域写法:
{ let tmp = ...; ... }
- ES5 闭包(执行函数表达式 IIFE)写法:
声明的函数在不同环境下可能会有差异,建议使用函数表达式,而不是函数声明语句
- 使用函数声明语句 -- 不推荐,不同环境下会有差异:
{ let a = 'secret'; function f() {return a;} }
- 浏览器环境: 函数声明类似于var(即会提升到全局作用域和函数作用域的头部)
- 其他环境:函数声明类似于let (对作用域之外没有影响)
- 使用函数表达式方式(推荐方式)
{ let a = 'secret'; let f = function () {return a;}; }
- 使用函数声明语句 -- 不推荐,不同环境下会有差异:
顶层对象 global
为同一段代码能够在各种环境,都能取到顶层对象,引入global 注:
- 浏览器中顶层对象: windows,self
- Node中顶层对象: global
- 一般通用方法是使用this,但有局限性
示例:
全局变量与顶层对象
- ES5中,全局变量与顶层对象等价
var a = 1; window.a // 1 -- Node 的 REPL 环境,可以写成 global.a,或者用通用方法this.a
- ES6中,全局变量与顶层对象不等价
let a = 1; window.a // undefined
- ES5中,全局变量与顶层对象等价
使用垫片库system.global取到global
// CommonJS 的写法 var global = require('system.global')(); // ES6 模块的写法 import getGlobal from 'system.global'; const global = getGlobal();
扩展运算符 ...
...变量
: 将剩余传入的参数值,存入一个数组变量中
...对象
: 拷贝对象的可遍历属性给一个新对象
示例:
function rest 参数
function add(...values) { let sum = 0; for (let val of values) { sum += val; } return sum; } add(2, 5, 3) // 10
- 注:rest参数只能是最后一个参数,否则会报错
(ES5 使用arguments对象,类似数组,但非数组,可使用Array.prototype.slice.call(arguments)转换为数组)
- 注:rest参数只能是最后一个参数,否则会报错
解构赋值
let [head, ...tail] = [1, 2, 3, 4]; head // 1 tail // [2, 3, 4]
拷贝对象的可遍历属性,同
Object.assign
let aClone = { ...a }; // 等同 let aClone = Object.assign({}, a); let abClone = { ...a, ...b }; // 等同 let abClone = Object.assign({}, a, b); let aWithOverrides = { ...a, x: 1, y: 2 }; let aWithOverrides = { ...a, ...{ x: 1, y: 2 } }; let x = 1, y = 2, aWithOverrides = { ...a, x, y }; // 以上都等同 let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });
- 注: 扩展运算符的参数对象之中,如果有取值函数get,这个函数是会执行
let obj={ a:1, get x(){ throw new Error('get x error!'); } } let c={...obj}; // get x error!
- 注: 扩展运算符的参数对象之中,如果有取值函数get,这个函数是会执行
解构赋值 (Destructuring)
从等式右边的对象中提取值,赋给左边对应变量:
- 模式匹配:只要等号两边的模式相同,左边的变量就会被赋予对应的值
- 类型转换:若等号右边的值不是对象或数组,就先将其转为对象
- 浅拷贝:解构赋值的拷贝都是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本
- 注:
undefined
/null
: 无法转为对象,对它们进行解构赋值,会报错...
:扩展运算符的解构赋值(...a
),只能读取对象自身的属性
eg:
let a = 1;
let b = 2;
// 可合并成:
let [a, b] = [1, 2];
a // 1
b // 2
解构数组赋值
右边数据结构具有 Iterator 接口,即可用数组形式解构赋值,否则报错
示例:
完全解构
let [foo, [[bar], baz]] = [1, [[2], 3]]; foo // 1 bar // 2 baz // 3 let [x, , y] = [1, 2, 3]; x // 1 y // 3 let [head, ...tail] = [1, 2, 3, 4]; head // 1 tail // [2, 3, 4]
不完全解构
let [a, [b], d] = [1, [2, 3], 4]; a // 1 b // 2 d // 4 let [x, y, ...z] = ['a']; x // "a" y // undefined z // []
解构不成功
let [foo] = []; foo // undefined //报错 let [foo] = 1; let [foo] = false; let [foo] = {}; let [foo] = null;
- 对数组进行对象属性的解构,使用:属性名表达式
let arr = [1, 2, 3]; let {0 : first, [arr.length - 1] : last} = arr; first // 1 last // 3
解构对象赋值
对象的属性没有次序,变量须与属性同名或给变量指定对应属性,才能取到正确的值 (解构数组赋值:是按照数组顺序位置给对应变量赋值的)
示例:
单层结构对象
let { foo, bar } = { foo: "aaa", bar: "bbb" }; foo // "aaa" bar // "bbb" let { x } = { foo: "aaa", bar: "bbb" }; x // undefined let { foo: x } = { foo: 'aaa', bar: 'bbb' }; x // aaa
嵌套结构的对象
let obj = { p: [ 'Hello',{ y: 'World' }] }; let { p: [x, { y }] } = obj; // 这时p是模式,不是变量,不会被赋值 x // "Hello" y // "World" let { p, p: [x, { y }] } = obj; p // ["Hello", {y: "World"}] x // "Hello" y // "World" // 嵌套对象,若子对象所在的父属性不存在,会报错 let {foo: {bar}} = {baz: 'baz'}; // 报错,因为foo不存在
解构函数参数赋值
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
函数add
的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x
和y
[[1, 2], [3, 4]].map(([a, b]) => a + b); // [ 3, 7 ]
解构基础类型赋值
会先转为对象
示例:
字符串
const [a, b, c, d, e] = 'hello'; a // "h" b // "e" let {length : len} = 'hello'; len // 5
- 数值
let {toString: s} = 123; s === Number.prototype.toString // true
- 布尔值
let {toString: s} = true; s === Boolean.prototype.toString // true
undefined
,null
: 无法转换为对象,解构报错let { prop: x } = undefined; // TypeError let { prop: y } = null; // TypeError
指定默认值
undefined
会触发使用默认值
示例:
数组
let [x, y = 'b'] = ['a']; // x='a', y='b' // 默认值可以是一个表达式,表达式是惰性求值的,即只有在用到的时候,才会求值 function f() { console.log('aaa'); } let [x = f()] = [1]; // x 能取到值,所以函数f根本不会执行 // 默认值可以引用解构赋值的其他变量,但该变量必须已经声明 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 -- 因为x用y做默认值时,y还没有声明
对象
// 注:默认值生效的条件是,对象的属性值严格等于(===)undefined var {x, y = 5} = {x: 1}; x // 1 y // 5 var {x: y = 3} = {}; y // 3 var {x = 3} = {x: null}; x // null
函数
function move({x = 0, y = 0} = {}) { // 为变量x,y指定默认值 return [x, y]; } move({x: 3, y: 8}); // [3, 8] move({x: 3}); // [3, 0] move({}); // [0, 0] move(); // [0, 0] function move({x, y} = { x: 0, y: 0 }) { // 为函数参数对象整体指定默认值,而不是为变量x和y指定默认值 return [x, y];, } move({x: 3, y: 8}); // [3, 8] move({x: 3}); // [3, undefined] move({}); // [undefined, undefined] move(); // [0, 0]
应用示例
交换变量的值
let x = 1; let y = 2; [x, y] = [y, x]; // x=2,y=1
合并数组
// ES5 var params=['hello',true,7]; var other=[1,2].concat(params); console.log(other); // ES6 // 利用扩展运算符合并数组 var params=['hello',true,7]; var other=[1,2,...params]; console.log(other);
函数返回多个值: 函数返回多个值,只能将它们放在数组或对象里,通过解构赋值,取出这些值很方便
function example() { return [1, 2, 3]; } let [a, b, c] = example();
函数参数的定义:方便地将一组参数与变量名对应起来
// 参数是一组有次序的值 function f([x, y, z]) { ... } f([1, 2, 3]); // 参数是一组无次序的值 function f({x, y, z}) { ... } f({z: 3, y: 2, x: 1});
提取 JSON 数据
let jsonData = { id: 42, status: "OK", data: [867, 5309] }; let { id, status, data: number } = jsonData; console.log(id, status, number); // 42, "OK", [867, 5309]
遍历 Map 结构 :任何部署了 Iterator 接口的对象,都可以用for...of循环遍历
const map = new Map(); map.set('first', 'hello'); map.set('second', 'world'); for (let [key, value] of map) { console.log(key + " is " + value); } // first is hello // second is world // 只获取键名 for (let [key] of map) { ...} // 只获取键值 for (let [,value] of map) { ...}
import模块的部分项
const { SourceMapConsumer, SourceNode } = require("source-map");
注: 圆括号
// 错误
let x;
{x} = {x: 1}; // SyntaxError: syntax error 因为{x}会被当成一个代码块
// 正确
let x;
({x} = {x: 1});
Iterator 遍历器
提供一种统一的遍历接口,供for...of
(或while
)消费(循环遍历)
本质:创建一个指针对象,通过
next
方法移动指针,指向遍历对象的成员,返回成员信息属性:
value
: 当前成员的值done
: 布尔值,表示遍历是否结束
方法:
next
:指针跳到下一个成员(遍历器必需部署此方法)return
: 循环遍历中提前退出(出错,break)时触发调用 (可选部署)- 注:必须返回一个对象
- 使用场景:一个对象在完成遍历前,需要清理或释放资源
throw
: 主要配合 Generator 函数使用(可选部署)
可遍历性(iterable):
- 部署了Iterator 接口的数据结构,此数据结构即是“可遍历的”
Symbol.iterator
属性:当前数据结构默认的遍历器生成函数(即Iterator
接口),执行这个函数,就会返回一个遍历器对象let arr = ['a', 'b', 'c']; let iter = arr[Symbol.iterator](); iter.next() // { value: 'a', done: false } iter.next() // { value: 'b', done: false } iter.next() // { value: 'c', done: false } iter.next() // { value: undefined, done: true }
原生具备
Iterator
接口的数据结构:- Array/TypedArray
- Set/Map
- String
- function的arguments对象
- Dom NodeList
- Generator对象
遍历操作:
for...of
循环- 循环读取键值(value)
- 遍历所有数据结构的统一的方法
- 内部调用的是数据结构的
Symbol.iterator
方法,可以与break
,continue
,return
配合使用(forEach
不行)
for...in
循环- 循环读取键名(key),
- 任意顺序,不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键
- 主要是为遍历对象而设计
示例:
遍历数组
let arr = ['a', 'b', 'c']; arr.foo = 'hello'; // for...in循环读取键名(key),注意:数组的key为数字,但循环键名为字符串 for (let i in arr) { console.log(i); // "0", "1", "2", "foo" } // for...of循环读取键值(value),注意:数组的遍历器接口只返回具有数字索引的属性 for (let i of arr) { console.log(i); // a,b,c -- 不会返回数组arr的foo属性 }
遍历对象
// 对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用 let es6 = { edition: 6, committee: "TC39", standard: "ECMA-262" }; for (let e of es6) { console.log(e); // TypeError: es6[Symbol.iterator] is not a function } // for...in可以遍历普通对象 for (let e in es6) { console.log(e); // edition,committee,standard }
Proxy 代理器
用于修改某些操作的默认行为,相当于在目标对象之前架设一层“拦截”,对外界的访问进行过滤和改写
Proxy 对象
构造Proxy实例对象:
方式一:
var proxy = new Proxy(target, handler);
var person = { name: "张三" }; var proxy = new Proxy(person, { get: function(target, property) { if (property in target) { return target[property]; } else { throw new ReferenceError("Property \"" + property + "\" does not exist."); } } }); proxy.name // "张三" proxy.age // 抛出一个错误
方式二:
let {proxy,revoke}=Proxy.revocable(target,handler);
: 生成一个可取消的 Proxy 实例- Proxy.revocable方法返回一个对象
- 该对象的proxy属性是Proxy实例
该对象的revoke属性是一个函数,可以取消Proxy实例
let target = {}; let handler = {}; let {proxy, revoke} = Proxy.revocable(target, handler); proxy.foo = 123; proxy.foo // 123 revoke(); proxy.foo // TypeError: Revoked
参数说明:
target
:所要拦截的目标对象handler
:对象,用于定制拦截行为,若为空对象{},则没有任何拦截效果,访问proxy对象等同于访问target对象
注:Proxy代理后,目标对象内部的
this
会指向Proxy代理对象const target = { m: function () { console.log(this === proxy); } }; const handler = {}; const proxy = new Proxy(target, handler); target.m() // false proxy.m() // true
Proxy 支持的拦截操作:
- 对象属性
get(target,propKey,receiver)
: 拦截对象属性的读取,eg:proxy.foo,proxy['foo']
set(target,propKey,value,receiver)
: 拦截对象属性的设置,eg:proxy.foo=v,proxy['foo']=v
has(target,propKey)
: propKey in proxydeleteProperty(target,propKey)
: delete proxy[propKey]
- 函数调用
apply(target,ctx,args)
: 拦截 Proxy 实例作为函数调用的操作,eg:proxy(...args)
,proxy.call(ctx,...args)
,proxy.apply(...)
construct(target,args)
: 拦截 Proxy 实例作为构造函数调用的操作,eg:new proxy(...args)
- 属性描述对象
defineProperty(target,propKey,propDesc)
: 拦截添加新属性,eg:Object.defineProperty(proxy, propKey, propDesc)
,Object.defineProperties(proxy, propDescs)
ownKeys(target)
: 拦截对象自身属性的读取操作, eg:Object.getOwnPropertyNames
,Object.getOwnPropertySymbols
,Object.keys
,for...in
getOwnPropertyDescriptor(target,propKey)
: 拦截获取属性描述对象, eg:Object.getOwnPropertyDescriptor(proxy, propKey)
- 对象原型
getPropertyOf(target)
: 拦截获取对象原型, eg:Object.getPrototypeOf(proxy)
,instanceof
setPropertyOf(target,proto)
: 拦截设置对象原型, eg:Object.setPrototypeOf(proxy, proto)
- 对象扩展
isExtensible(target)
: Object.isExtensible(proxy)preventExtensions(target)
: Object.preventExtensions(proxy)
应用示例:
Proxy对象作为普通函数调用 VS 作为构造函数调用
var handler = { get: function(target, name) { if (name === 'prototype') { return Object.prototype; } return 'Hello, ' + name; }, apply: function(target, thisBinding, args) { return args[0]; }, construct: function(target, args) { return {value: args[1]}; } }; var fproxy = new Proxy(function(x, y) { return x + y; }, handler); fproxy(1, 2) // 1 new fproxy(1, 2) // {value: 2} fproxy.prototype === Object.prototype // true fproxy.foo === "Hello, foo" // true
私有变量
ES3 写法
var Person=function(){ var data={ name:'Tom', sex:'male', age:15 } this.get=function(key){ return data[key]; } this.set=function(key,value){ if(key!=='sex') data[key]=value; } } var person=new Person(); person.set('name','Jack'); person.set('sex','female'); console.table({ name: person.get('name'), sex: person.get('sex'), age: person.get('age') }); // Jack,male,15
- ES5 写法
var Person={ name:'Tom', age: 15 } Object.defineProperty(Person,'sex',{ writable:false, value:'male' }) Person.name='Jack'; console.table({ name: Person.name, age: Person.age, sex: Person.sex }); // Jack,male,15 Person.sex='female'; // will throw exception
ES6
let Person={ name:'Tom', sex:'male', age:15 }; let person=new Proxy(Person,{ get(target,key){ return target[key] } set(target,key){ if(key!=='sex') target[key]=value; } }); person.set('name','Jack'); console.table({ name: person.get('name'), sex: person.get('sex'), age: person.get('age') }); // Jack,male,15 person.set('sex','female'); // will throw exception
Reflect 对象
将Object的一些方法放到Reflect上,使用Reflect代替Object的一些方法,例如:
defineProperty方法
// 老写法 try { Object.defineProperty(target, property, attributes); // success } catch (e) { // failure } // 新写法 if (Reflect.defineProperty(target, property, attributes)) { // success } else { // failure }
判断对象是否有某属性
// 老写法 'assign' in Object // true // 新写法 Reflect.has(Object, 'assign') // true
方法调用
// 老写法 Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1 // 新写法 Reflect.apply(Math.floor, undefined, [1.75]) // 1
与Proxy对象的方法一一对应,可通过Reflect获取对象原有的默认行为,例如:
var loggedObj = new Proxy(obj, { get(target, name) { console.log('get', target, name); return Reflect.get(target, name); }, deleteProperty(target, name) { console.log('delete' + name); return Reflect.deleteProperty(target, name); }, has(target, name) { console.log('has' + name); return Reflect.has(target, name); } });
对象 Object
对象属性
Descriptor
属性描述对象: 对象的每个属性都有一个描述对象,用来控制该属性的行为
数据属性描述对象包含:
- value
- writable
- enumerable
- configurable
获取对象自身属性(非继承属性)的描述对象
- Object.getOwnPropertyDescriptor
- Object.getOwnPropertyDescriptors
- Reflect.getOwnPropertyDescriptors
某属性的描述对象的enumerable:可枚举性,若为false,即不可枚举,则一下操作会忽略该属性
- for...in循环
- Object.keys()
- JSON.stringify()
- Object.assign()
- 注:
- 以上操作除了
for...in
会包含继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性 - ES6 规定,所有 Class 的原型的方法都是不可枚举的
- 以上操作除了
__prop__
属性 (前后各两个下划线): 等于Object.prototype.__proto__
,即一个对象的__prop__
属性值就是对象的原型
- 操作对象的prototype对象(原型对象)的方法:
- Object.setPrototypeOf(object, prototype);
- Object.getPrototypeOf(object);
- Object.create(...)
获取对象自身属性的操作 (即不包括继承属性):
- 可枚举属性(无Symbol):Object.keys(obj) -- ES2017 引入了Object.values,Object.entries,作为遍历一个对象的补充手段,供for...of循环使用
- 可枚举和不可枚举属性(无Symbol):Object.getOwnPropertyNames(obj)
- Symbol属性:Object.getOwnPropertySymbols(obj)
- 所有(可枚举,不可枚举,Symbol):Reflect.ownKeys(obj)
示例:
Descriptor 属性描述对象
const obj = { foo: 123, get bar() { return 'abc' } }; Object.getOwnPropertyDescriptor(obj, 'foo') Object.getOwnPropertyDescriptors(obj) // descriptor对象 // { foo: // { value: 123, // writable: true, // enumerable: true, // 可枚举性 // configurable: true // }, // bar: // { get: [Function: get bar], // set: undefined, // enumerable: true, // configurable: true } // }
读取/遍历对象
let obj = { a: 1, b: 2, c: 3 }; Object.keys(obj) // ['a', 'b'] Object.values(obj) // [1,2,3] Object.entries(obj) // [ ['a', 1], ['b', 2],['c',3] ] for (let [key, value] of entries(obj)) { console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3] } const map = new Map(Object.entries(obj)); // Map {a: 1, b: 2, c: 3}
对象比较
==
相等运算符:自动转换数据类型===
严格相等运算符:NaN不等于自身,+0与-0相等Object.is
同值相等:比较两个值是否严格相等,与===
相比,NaN
等于自身,+0
与-0
不等
+0 === -0 //true
NaN === NaN // false
Object.is(+0, -0) // false
Object.is(NaN, NaN) // true
Object.is('foo', 'foo') // true
Object.is({}, {}) // false
对象拷贝
浅拷贝: 只能进行值的复制,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用
Object.assign(target,src1,src2,...)
: 拷贝可被枚举的自有属性到目标对象(浅拷贝,同名属性替换, 取值函数求值后再复制)// 浅拷贝,同名属性替换 const target = { a: 1, b: 1,d:{e:'hello',f:'world'} }; const source1 = { b: 2, c: 2 }; const source2 = { c: 3,d:{g:'say'} }; Object.assign(target, source1, source2); // {a:1, b:2, c:3,d:{g:'say'}}
- 参数注意点:
- 只有一个参数,即只有
target
,则返回target
(不是对象会先转换成对象返回) - 传入不是对象的参数,会先转成对象(eg:字符串可转换为字符数组,数组视为属性名为 0、1、2 的对象)
- 传入无法转成对象的参数(eg: undefined,null,数值,布尔值):
- 作为第一个参数(即target)会报错;
- 不是第一个参数(即source),会跳过
- 只有一个参数,即只有
- 只拷贝属性值,不会拷贝它背后的赋值方法或取值方法,取值函数求值后再复制值
// 不会复制取值函数,会用取值函数求值后再复制 const source = { get foo() { return 1 } }; Object.assign({}, source) // { foo: 1 }
使用
Object.getOwnPropertyDescriptors方法
配合Object.defineProperties方法
添加描述对象,可实现正确拷贝// 配合Object.defineProperties方法添加描述对象 const source = { set foo(value) { console.log(value); } }; const target = {}; Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); Object.getOwnPropertyDescriptor(target, 'foo'); // { // get: undefined, // set: [Function: set foo], // enumerable: true, // configurable: true //}
- 参数注意点:
Object.create(proto [, propertyDescriptors ])
:创建一个新对象,对象继承到__proto__
属性上const person = { isHuman: false, printIntroduction: function () { console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`); } }; const me = Object.create(person); me.name = "Matthew"; // "name" is a property set on "me", but not on "person" me.isHuman = true; // inherited properties can be overwritten me.printIntroduction(); // "My name is Matthew. Am I human? true"
proto
:新创建对象的原型对象,可为nullpropertyDescriptors
:可选项,新对象属性的描述对象(其自身定义的属性,不是其原型链上的属性)let o = Object.create({}, { p: { value: 42 } }) // 创建一个以空对象为原型,拥有一个属性p的对象 // 省略了的属性特性默认为false,所以属性p是不可写,不可枚举,不可配置的 // p的属性描述对象的enumerable默认是false, 改成true,Object.values就会返回属性p的值 Object.values(obj) // [] o.p // 42 o.p = 20 // 失败 o.__proto__ // Object {} o.__proto__.p // undefined
var o = Object.create(Object.prototype, { foo: { // foo会成为所创建对象的数据属性 writable:true, configurable:true, value: "hello" }, bar: { // bar会成为所创建对象的访问器属性 configurable: false, // false,下面set,get方法不起作用 get: function() { return 10 }, set: function(value) { console.log("Setting `o.bar` to", value); } } }); console.log(o); // {foo:'hello'}
Object.create()
,new Object()
,{}
区别// test1,test2,test3的__proto 一样 var test1 = {}; var test2 = new Object(); var test3 = Object.create(Object.prototype); // 创建一个原型为null的对象,test4.__proto__为undefined, 没有继承原型属性和方法,不同于test1,2,3 var test4 = Object.create(null); var test = Object.create({x:123,y:345}); console.log(test); //{} console.log(test.x); //123 console.log(test.__proto__.x); //123 console.log(test.__proto__.x === test.x); //true var test1 = new Object({x:123,y:345}); console.log(test1); //{x:123,y:345} console.log(test1.x); //123 console.log(test1.__proto__.x); //undefined console.log(test1.__proto__.x === test1.x); //false var test2 = {x:123,y:345}; console.log(test2); //{x:123,y:345}; console.log(test2.x); //123 console.log(test2.__proto__.x); //undefined console.log(test2.__proto__.x === test2.x); //false
综合示例: 克隆一个对象(包括对象原型的属性,浅拷贝)
// 写法一,注:__proto__属性在非浏览器的环境不一定部署,因此推荐使用写法二和写法三
const clone1 = {
__proto__: Object.getPrototypeOf(obj),
...obj
};
// 写法二
const clone2 = Object.assign(
Object.create(Object.getPrototypeOf(obj)),
obj
);
// 写法三
const clone3 = Object.create(
Object.getPrototypeOf(obj),
Object.getOwnPropertyDescriptors(obj)
);
super对象
指向当前对象的原型对象
示例:
调用当前对象原型对象的属性
const proto = { foo: 'hello'}; const obj = { foo: 'world', find() { return super.foo; // 引用了原型对象proto的foo属性,同 Object.getPrototypeOf(this).foo } }; Object.setPrototypeOf(obj, proto); obj.find() // "hello"
调用当前对象原型对象的方法
const proto = { x: 'hello', foo() { console.log(this.x); }, }; const obj = { x: 'world', foo() { super.foo(); // 同 Object.getPrototypeOf(this).foo.call(this),this绑定的是当前obj } } Object.setPrototypeOf(obj, proto); obj.foo() // "world"
注: 只能用在对象的方法中(注:方法为简写方式才可以让 JavaScript 引擎确认,定义的是对象的方法)
- super用在属性里面,报错
const obj = { foo: super.foo }
super用在一个函数里面,然后赋值给foo属性,报错
// 错 const obj = { foo: () => super.foo } // 错 const obj = { foo: function () { return super.foo } }
- super用在属性里面,报错
函数 function
name属性
返回函数的函数名
function foo() {}
foo.name // "foo"
const a = function baz() {};
a.name // "baz"
注:
- 匿名函数: ES5返回空字符串,ES6返回赋给的变量名
var f = function () {}; f.name // ES5 ""; ES6 "f"
- Function构造函数返回的函数实例: anonymous
(new Function).name // "anonymous"
bind返回的函数: name属性值会加上
bound
前缀function foo() {}; foo.bind({}).name // "bound foo" (function(){}).bind({}).name // "bound "
函数参数
通过解构赋值设置参数
function add([x, y]){ // 参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量x和y return x + y; } add([1, 2]); // 3 [[1, 2], [3, 4]].map(([a, b]) => a + b); // [ 3, 7 ]
处理可变参数: 使用rest参数
...参数名
(类似ES5的arguments)function add(...values) { // values同ES5 Array.prototype.slice.call(arguments); let sum = 0; for (let val of values) { sum += val; } return sum; } add(2, 5, 3) // 10
参数默认值
直接写在参数定义的后面
function log(x, y = 'World') { console.log(x, y); } log('Hello') // Hello World log('Hello', 'China') // Hello China
可以使用表达式/函数(惰性求值)
let x = 99; function foo(p = x + 1) { console.log(p); } foo() // 100 x = 100; foo() // 101
使用解构赋值设置默认值
/* 1. 为函数参数对象整体指定默认值,eg: 为{x,y}对象整体指定默认值,而不是为变量x和y指定默认值 */ function m2({x, y} = { x: 0, y: 0 }) { return [x, y]; } m2() // [0,0] m2({}) // [undefined,undefined] m2({x: 3}) // [3, undefined] m2({x:3,y:8}) // [3, 8] /* 2. 为函数某个具体参数指定默认值,eg:为变量y指定默认值 */ function foo({x, y = 5} = {}) { console.log(x, y); } foo() // undefined 5 foo({}) // undefined 5 foo({x:3}) // 3 5 foo({x:3,y:8}) // 3 8
注:指定了默认值后,函数的length属性将失真,会返回没有指定默认值的参数个数
// 函数的length属性: 函数预期传入的参数个数 (function(...args) {}).length // 0 (function (a) {}).length // 1 // 设置默认参数后,函数的length属性将失真: (function (a, b, c = 5) {}).length // 2 (function (a, b = 1, c) {}).length // 1 -- 默认值以后的参数也不计数
应用示例:
- 利用参数默认值,指定某一个参数不得省略,若省略就抛出一个错误
function throwIfMissing() { throw new Error('Missing parameter'); } function foo(mustBeProvided = throwIfMissing()) { return mustBeProvided; } foo() // Error: Missing parameter
- 利用参数默认值,指定某一个参数是可以省略的(将参数默认值设为undefined)
function foo(optional = undefined) { //··· }
函数绑定运算符 ::
用来取代call、apply、bind调用
对象::函数
: 会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面bar.bind(foo); // ES5 foo::bar; // ES6 bar.apply(foo, arguments); // ES5 foo::bar(...arguments); // ES6
::对象.方法
:等于将该方法绑定在该对象上面var log = console.log.bind(console); // ES5 let log = ::console.log; // ES6, 同 let log = console::console.log;
- 若双冒号运算符的运算结果,还是一个对象,可采用链式写法
箭头函数 arrow-function
简化函数编写形式
// ES3,ES5
function a(){
exp
}
// ES6
// 只有一个参数,可省略"()"
// 表达式直接作为返回值时,可省略"{}"
(arg)=>{
exp
}
示例:
var f = function () { return 5 }; // ES5
var f = () => 5; // ES6
var sum = function(num1, num2) { return num1 + num2;}; // ES5
var sum = (num1, num2) => num1 + num2; // ES6
[1,2,3,4,5].map(function(v){ return v+1; }); // ES5
[1,2,3,4,5].map(v=>v+1); // ES6
//无返回
let fn = () => void doesNotReturn();
//返回一个对象(为防止语法歧义报错,用圆括号包起来)
let getTempItem = id => ({ id: id, name: "Temp" });
//使用rest参数
const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5) // [1,[2,3,4,5]]
限制:
- 不能用作构造函数(即不能用new)
- 不能使用arguments对象,用rest参数代替
- 不能用作Generator函数(即不能使用yield)
this
对象: 在箭头函数中,this对象的指向是固定的,为定义时所在的对象,不是使用时所在的对象
- 实际上:箭头函数没有自己的
this
、arguments
、super
、new.target
,只能引用外层代码块的对应变量 - 因为没有自己的
this
(使用外层代码块的this
),所以不能用作构造函数,也不能使用call
,apply
,bind
这些方法去改变this
的指向
示例:
ES3,ES5 原始写法 : this 指向的是该函数被调用的对象
var factory=function(){ this.a='a'; this.b='b'; this.c={ a:'a+', b:function(){ return this.a; } } } console.log(new factory().c.b()); // a+ -- this指向c
ES6 箭头函数 : this 指向的是定义时this的指向
var factory=function(){ this.a='a'; this.b='b'; this.c={ a: 'a+', b: ()=>{ return this.a; } } } console.log(new factory().c.b()); // a -- 同外层代码this,指向factory
箭头函数转成 ES5写法(注意this)
// ES6 箭头函数 function foo() { setTimeout(() => { console.log('id:', this.id); // this -- foo }, 100); } // ES5 原始写法 function foo() { var _this = this; setTimeout(function () { console.log('id:', _this.id); }, 100); }
优化:尾调用,尾递归
尾调用: 函数的最后一步是返回调用另一个函数
function f(x){
return g(x);
}
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,直接用内层函数的调用帧取代外层函数的调用帧即可 注:
- ES6支持尾调用优化,且只在严格模式下开启
- 只有不再用到外层函数的内部变量才可取代
例:以下三种情况,都不属于尾调用
function f(x){ let y = g(x); return y; // 因为调用后还有赋值操作 } function f(x){ return g(x) + 1; // 因为调用后还有操作 } function f(x){ g(x); // 函数最后一步为 return undefined; }
尾递归: 尾调用自身
函数调用自身,因为调用栈太多,容易发生“栈溢出”错误(stack overflow); 而尾递归,由于只存在一个调用帧,所以不会发生“栈溢出”错误
应用示例:
- 计算n的阶乘:
n!
- 非尾递归实现: 最多需要保存n个调用记录,复杂度 O(n)
function factorial(n) { if (n === 1) return 1; return n * factorial(n - 1); } factorial(5) // 120
- 尾递归实现(将所有用到的内部中间变量改写成函数的参数): 只保留一个调用记录,复杂度 O(1)
function factorial(n, total=1) { if (n === 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120
- 非尾递归实现: 最多需要保存n个调用记录,复杂度 O(n)
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
注: 尾递归优化只在严格模式下生效,正常模式下可采用“循环”换掉“递归”的方式进行优化
Symbol
- ES6新增的原始数据类型,类似于字符串的数据类型 (Javascript其他原始数据类型有:undefined,null,Boolean,String,Number,Object)
- 表示独一无二的值 (例如:可以用来保证对象的属性名是独一无二的)
- 通过
Symbol()
函数生成,可以接受一个字符串作为参数,表示对Symbol实例的描述 - 注:不能使用
new
,基本上,它是一种类似于字符串的数据类型
let s = Symbol();
typeof s // "symbol"
let s1 = Symbol('foo');
s1 // Symbol(foo)
s1.toString() // "Symbol(foo)"
//相同参数的Symbol函数的返回值是不相等
let s2 = Symbol('foo');
s1 === s2 // false
// 用于对象属性
let mySymbol = Symbol();
let a = {
[mySymbol]: 'Hello!'
};
a[mySymbol] // "Hello!"
Set/WeakSet
Set
- 类似于数组,但是成员的值都是唯一,可枚举(
Array.from
方法可以将 Set 结构转为数组) - 可以接受一个具有 iterable 接口的数据结构作为参数(例如数组),用来初始化
- 内部使用同值相等判断两个值是否相同(比严格相等
===
,多了NaN和0的比较),注:两个对象总是不相等的 - 属性:
Set.prototype.constructor
:构造函数,默认就是Set函数。Set.prototype.size
:返回Set实例的成员总数。
- 方法(操作):
- add(value)
- delete(value)
- has(value)
- clear()
- 方法(遍历):
- keys()
- values()
- entries()
- forEach()
示例:
无参构造使用Set
let s= new Set(); s.add(1).add(2).add(2); s.size // 2 s.has(1) // true s.has(2) // true s.has(3) // false s.delete(2); s.has(2) // false
可枚举对象作为参数构造
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]); items.size // 5 const s = new Set(); [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x)); // 去除数组的重复成员 [...new Set(array)]
遍历
let s2= new Set(['red', 'green', 'blue']); s2.keys() // ['red', 'green', 'blue'] s2.values() // ['red', 'green', 'blue'] s2.entries() // [ ['red','red'], ['green','green'], ['blue','blue'] ] // for...of循环遍历 for (let x of s2) { // 默认遍历器生成函数就是它的values方法 console.log(x); } // red // green // blue // forEach循环遍历 s2.forEach((value, key) => console.log(key + ' : ' + value)) // red:red // green:green // blue:blue
- 注:
keys/values()
Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致,默认遍历器生成函数就是values方法
- 注:
应用:Set 实现并集(Union)、交集(Intersect)和差集(Difference)
let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); // 并集 let union = new Set([...a, ...b]); // Set {1, 2, 3, 4} // 交集 let intersect = new Set([...a].filter(x => b.has(x))); // set {2, 3} // 差集 let difference = new Set([...a].filter(x => !b.has(x))); // Set {1}
WeakSet
- 与Set区别:
- 成员只能是对象,而不能是其他类型的值
- 成员对象都是弱引用,随时可能消失(即如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中)
- 垃圾回收机制运行前后可能会导致成员数不一样,所以ES6 规定 WeakSet不可遍历
- 方法:
- add(value)
- delete(value)
- has(value)
示例:
构造使用 WeakSet
const ws = new WeakSet(); const obj = {}; const foo = {}; ws.add(window); ws.add(obj); ws.has(window); // true ws.has(foo); // false ws.delete(window); ws.has(window); // false // WeakSet 没有size属性,不能遍历:因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在 ws.size // undefined ws.forEach // undefined
有参构造 WeakSet(注:WeakSet的成员只能是对象)
// 可以接受具有 Iterable 接口的对象 const a = [[1, 2], [3, 4]]; const ws1 = new WeakSet(a); // WeakSet {[1, 2], [3, 4]} 注:a数组的成员成为 WeakSet 的成员,不是a数组本身 const b = [3, 4]; const ws2 = new WeakSet([3, 4]); // Uncaught TypeError: Invalid value used in weak set(…) 注:b数组的成员不是对象
应用:Weakset 储存 DOM 节点,不用担心这些节点从文档移除时,会引发内存泄漏
const foos = new WeakSet() // foos对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑foos,也不会出现内存泄漏 class Foo { constructor() { foos.add(this) } method () { if (!foos.has(this)) { throw new TypeError('Foo.prototype.method 只能在Foo的实例上调用!'); } } }
Map/WeakMap
Map
- 键值对集合,类似对象(Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,key不限于字符串,各种类型的值包括对象都可以当作键,是一种更完善的 Hash 结构实现)
- 键唯一,跟内存地址绑定的,只要内存地址不一样,就视为两个键(0和-0是一个键,NaN视为同一个键)
- Map 的遍历顺序就是插入顺序
- 属性:
- size
- 方法(操作):
- set(key,value)
- get(key)
- has(key)
- delete(key)
- clear()
- 方法(遍历):
- keys()
- values()
- entries()
- forEach()
示例:
无参数构造
const m = new Map(); const o = {p: 'Hello World'}; m.set(o, 'content') m.get(o) // "content" m.set(1,'Hello') m.get(1) // "Hello" m.get('a') // undefined m.set(undefined, 3); m.get(undefined) // 3 m.set(['a'], 555); m.get(['a']) // undefined m.set(1,'a').set(2,'b')
可枚举对象作为参数构造
const m= new Map([ ['name', '张三'], ['title', 'Author'] ]); // Map {'name':'张三','title':'Author'} // 相当于 param.forEach( ([key, value]) => m.set(key, value) ); m.size // 2 m.has('name') // true m.get('name') // "张三"
遍历
m.keys() // ['name','title'] m.values() // ['张三','Author'] m.entries() // [ ['name', '张三'],['title', 'Author'] ] // forEach循环遍历 m.forEach(function(value, key, map) { console.log("Key: %s, Value: %s", key, value); }); // for...of循环遍历 for (let [key, value] of map) { console.log(key, value); } for (let [key, value] of map.entries()) { console.log(key, value); } for (let item of map.entries()) { console.log(item[0], item[1]); } // name 张三 // title Author
- 注:
entries()
是Map结构的默认遍历器接口(部署在Symbol.iterator属性上,即map[Symbol.iterator] === map.entries
)
- 注:
Map <-> Array 转换
- Map -> Array: 使用扩展运算符
...
const myMap = new Map() .set(true, 7) .set({foo: 3}, ['abc']); [...myMap] // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
- Array -> Map: 直接作为参数传入Map构造函数
new Map([ [true, 7], [{foo: 3}, ['abc']] ]) // Map { true:7, {foo:3}:['abc'] }
- Map -> Array: 使用扩展运算符
Weakmap
- 与Map区别:
- 只接受对象作为键名(null除外)
- 键名所指向的对象为弱引用(不计入垃圾回收机制,即一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用)
- 注:WeakMap 弱引用的只是键名,而不是键值,所以即使在 WeakMap 外部消除了键值的引用,WeakMap 内部的引用依然存在
- 没有遍历操作(即没有keys()、values(),entries(),forEach方法),也没有size属性
- 无法清空,即不支持clear方法
- 方法:
- get()、set()、has()、delete()
- 应用(WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏)
- DOM 节点作为键名(在网页的 DOM 元素上添加数据,当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除,有助于防止内存泄漏)
- 部署私有属性
示例:
构造使用 WeakMap
const wm = new WeakMap(); const key = {foo: 1}; wm.set(key, 2); // set 添加成员 wm.get(key) // get 获取成员 // 只接受对象作为键名(null除外) wm.set(1, 2) // TypeError: 1 is not an object! wm.set(Symbol(), 2) // TypeError: Invalid value used as weak map key // size、forEach、clear 方法都不存在 wm.size // undefined wm.forEach // undefined wm.clear // undefined // 可接受一个数组,作为构造函数的参数 const k1 = [1, 2, 3]; const k2 = [4, 5, 6]; const wm2 = new WeakMap([[k1, 'foo'], [k2, 'bar']]); wm2.get(k2) // "bar"
应用:DOM 节点作为键名存储在Weakmap中,防止内存泄漏
// myElement是一个 DOM 节点,每当发生click事件,就更新一下状态 // 一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险 let myElement = document.getElementById('logo'); let myWeakmap = new WeakMap(); myWeakmap.set(myElement, {timesClicked: 0}); myElement.addEventListener('click', function() { let logoData = myWeakmap.get(myElement); logoData.timesClicked++; }, false);
应用:Weakmap 部署私有属性,实例消失,它们也就随之消失
// Countdown类的两个内部属性_counter和_action,是实例的弱引用 // 如果删除实例,它们也就随之消失,不会造成内存泄漏 const _counter = new WeakMap(); const _action = new WeakMap(); class Countdown { constructor(counter, action) { _counter.set(this, counter); _action.set(this, action); } dec() { let counter = _counter.get(this); if (counter < 1) return; counter--; _counter.set(this, counter); if (counter === 0) { _action.get(this)(); } } } const c = new Countdown(2, () => console.log('DONE')); c.dec() c.dec() // DONE
类 Class
类的数据类型就是函数,类本身就指向构造函数
constructor
构造函数- 默认返回实例对象,即this(this 代表实例对象)
- 无参构造,可以不显式定义,会默认添加一个空的constructor方法
new
创建实例对象new 构造函数(args)
: 从prototype对象生成一个实例对象 ( 注:ES6 class必须使用new调用,否则会报错 )new.target
: 一般在构造函数中调用,返回new命令作用于的那个构造函数,若不是用new命令调用,返回undefined (这个属性可以用来确定构造函数是怎么调用的)
类成员:
- 变量:定义在this上,是类的实例对象自身的属性,属于类实例对象
class Point { constructor(x, y) { this.x = x; this.y = y; } }
- 提案:实例属性: 用等式直接写入类的定义之中(以前只能写在类的constructor方法里面)
class MyClass { myProp = 42; constructor() { console.log(this.myProp); // 42 } }
- 提案:实例属性: 用等式直接写入类的定义之中(以前只能写在类的constructor方法里面)
- 方法: 定义在class上,是原型对象prototype的属性,属于类
class Point { say(){ console.log("Hello"); } }
注:
getter/setter
对某个属性设置存值函数和取值函数(部署在Descriptor属性描述对象上),拦截该属性的存取行为class MyClass { get prop() { return 'getter'; } set prop(value) { console.log('setter: '+value); } } let inst = new MyClass(); inst.prop = 123; // setter: 123 inst.prop // 'getter'
私有属性/方法:
利用
Symbol
值的唯一性,将私有方法的名字命名为一个Symbol值const bar = Symbol('bar'); const snaf = Symbol('snaf'); export default class myClass{ // 公有方法 foo(x) { this[bar](x); } // 私有方法 [bar](x) { return this[snaf] = x; } }
- 提案:使用
#
表示class Foo { #a; #b; #sum() { return #a + #b; } printSum() { console.log(#sum()); } constructor(a, b) { #a = a; #b = b; } }
静态属性/方法: 直接通过类来调用(实例上调用,会抛出错误,表示不存在)
静态属性:ES6 没有静态属性,可在类外部定义实现
class Foo {} Foo.prop = 1; // 为Foo类定义了一个静态属性prop Foo.prop // 1
提案:在实例属性写法前面加上static关键字
class MyClass { static myStaticProp = 42; constructor() { console.log(MyClass.myStaticProp); // 42 } }
静态方法: 可以与非静态方法重名 ( 注:static方法中的this指向类,不是实例 )
class Foo { static classMethod() { return 'hello'; } } Foo.classMethod() // 'hello' var foo = new Foo(); foo.classMethod() // TypeError: foo.classMethod is not a function
示例:
定义使用类
ES5方式
// 构造函数: function Point(x, y) { this.x = x; this.y = y; } // 类的所有方法都定义在类的prototype属性上面: Point.prototype={ constructor:Point, toString:function(){ return '(' + this.x + ', ' + this.y + ')';}; } } // 使用 var p = new Point(1, 2); console.log(p.toString());
ES6方式
class Point { constructor(x, y) { this.x = x; this.y = y; } toString() { return '(' + this.x + ', ' + this.y + ')'; } } var p = new Point(1, 2); console.log(p.toString());
注:
- 类的数据类型就是函数,类本身就指向构造函数
typeof Point // "function" Point === Point.prototype.constructor // true
- 类属性定义在this变量上,属于类实例对象; 类方法定义在class上 ,属于类
p.hasOwnProperty('x') // true p.hasOwnProperty('y') // true p.hasOwnProperty('toString') // false p.__proto__.hasOwnProperty('toString') // true
类的内部所有定义的方法,ES6定义的不可枚举,ES5方式定义的可枚举
/* ES5 */ Object.keys(Point.prototype) // ["toString"] Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"] /* ES6 */ Object.keys(Point.prototype) // [] Object.getOwnPropertyNames(Point.prototype) // ["constructor","toString"]
- ES6 class必须使用new调用,否则会报错
var point = Point(2, 3); // 报错 var point = new Point(2, 3); // 正确
- 类的数据类型就是函数,类本身就指向构造函数
无参构造类( constructor可以不显式定义)
ES5方式
/* Javascript */ function Point() {} Point.prototype.toString=function(){...} // 等同于 function Point(){} Point.prototype={ constructor:Point, toString:function(){...} }
ES6方式
class Point{ toString(){...} } // 等同于 class Point { constructor() {} toString(){...} }
匿名类
let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }('张三'); person.sayName(); // "张三"
new.target
: 用在构造函数中调用,返回new命令作用于的那个构造函数ES5方式
function Person(name) { if (new.target === Person) { this.name = name; } else { throw new Error('必须使用 new 命令生成实例'); } } var person = new Person('张三'); // 正确 var notAPerson = Person.call(person, '张三'); // throw Error: 必须使用 new 命令生成实例
ES6
class Rectangle { constructor(length, width) { console.log(new.target === Rectangle); this.length = length; this.width = width; } } var obj = new Rectangle(3, 4); // 输出 true var obj2 = Rectangle(2, 3); // 报错,ES6 class必须使用new调用
应用:创建不能独立使用、必须继承后才能使用的类
//Class内部调用new.target,返回当前Class;子类继承父类时,返回子类 class Shape { constructor() { if (new.target === Shape) { throw new Error('本类不能实例化'); } } } class Rectangle extends Shape { constructor(length, width) { super(); // ... } } var x = new Shape(); // 报错 var y = new Rectangle(3, 4); // 正确
extened 继承
class A {
constructor() {
console.log(new.target.name); // new.target 指向new命令作用于的那个构造函数
}
}
class B extends A {
constructor() {
super(); // 相当于 A.prototype.constructor.call(this), 这里this指的是B的实例 -- super代表父类的构造函数
}
}
new A() // A
new B() // B
继承机制:
- ES5:创造子类的实例对象this,再将父类的方法添加到this上面
- ES6:将父类实例对象的属性和方法加到this上面,再用子类的构造函数修改this
- 子类必须在constructor方法中调用super方法,且只有调用super之后,才可以使用this关键字,否则新建实例时会报错
- 子类会继承父类的静态方法,也可通过super对象调用父类的静态方法
- 获取对象原型:
Object.getPrototypeof(obj)
- 可以用来从子类上获取父类,使用这个方法判断,一个类是否继承了另一个类
- eg:
Object.getPrototypeOf(ColorPoint) === Point
为 true
原生构造函数的继承(原生类的继承):
- 语言内置的构造函数,ES5无法继承,ES6可以( ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承)
- ECMAScript 的原生构造函数:
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
特例:继承Object的子类,无法通过super方法向父类Object传参
class NewObj extends Object{ constructor(){ super(...arguments); } } var o = new NewObj({attr: true}); o.attr === true // false
- 因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过
new Object()
这种形式调用,ES6 规定Object构造函数会忽略参数
- 因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过
应用实例:定义一个带版本功能的数组
class VersionedArray extends Array { constructor() { super(); this.history = [[]]; } commit() { this.history.push(this.slice()); } revert() { this.splice(0, this.length, ...this.history[this.history.length - 1]); } } var x = new VersionedArray(); x.push(1); x.push(2); x // [1, 2] x.history // [[]] x.commit(); x.history // [[], [1, 2]] x.push(3); x // [1, 2, 3] x.history // [[], [1, 2]] x.revert(); x // [1, 2]
super/this 关键字
this: 一般指向该方法运行时所在的环境
- 在类中:
- 普通方法中:指向类的实例;
- 静态方法中:指向类;
- 在子类中:
- 普通方法中:指向子类实例;注:用super对象调用父类方法时,父类方法中的this指向的也是子类实例
- 静态方法中:指向子类;注:用super对象调用父类方法时,父类方法中的this指向的也是子类
- 在箭头函数中:
- 指向是固定的,为定义时所在的对象,不是使用时所在的对象(因为箭头函数没有自己的this,只能使用外层代码块的this)
- 注:箭头函数不能用作构造函数,也不能使用call,apply,bind这些方法去改变this的指向
示例:
class Logger {
printName(name = 'there') {
this.print(`Hello ${name}`); // this 指向类的实例
}
print(text) {
console.log(text);
}
}
const logger = new Logger();
logger.printName(); // Hello there
注:单独使用类方法,而此类方法中使用this调用其他类方法,可能会报错,eg:将printName方法提取出来单独使用会报错
const { printName } = logger;
printName(); // TypeError: Cannot read property 'print' of undefined
// 解决方案: 在构造方法中绑定this
class Logger {
constructor() {
this.printName = this.printName.bind(this);
}
// ...
}
super
super(...)
: super方法代表父类的构造函数,只能用在子类的构造函数之中,用在其他地方就会报错super.xxx
: super对象一般指向当前对象的原型对象, 只能用在对象的方法中- 在子类:
- 普通方法中: super指向父类的原型对象;注:通过super调用父类的方法时,父类方法内部的this指向的是子类实例
- 静态方法中:super指向父类本身;注:通过super方法调用父类的方法时,父类方法内部的this指向的是子类
- 在子类:
- 注:使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错
示例:
super方法 vs super对象
class Point { constructor(x, y) { this.x = x; this.y = y; } p() { return 2; } } class ColorPoint extends Point { constructor(x, y, color) { this.color = color; // ReferenceError super(x, y); // 调用父类构造函数 this.color = color; // 正确,super之后,才可以使用this console.log(super.p()); // 2 -- super指向父类原型对象,相当于Point.prototype.p() console.log(super); // 报错: 使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错 // super.valueOf()表明super是一个对象,指向父类原型对象,super使得this指向B的实例,所以返回的是一个B的实例 console.log(super.valueOf() instanceof B); // true } } let cp = new ColorPoint(25, 8, 'green'); cp instanceof ColorPoint // true cp instanceof Point // true
super对象调用父类静态方法和普通方法
class Parent { static hello() { console.log('hello world'); } static myMethod(msg) { console.log('static', msg); } myMethod(msg) { console.log('instance', msg); } } class Child extends Parent { static myMethod(msg) { super.myMethod(msg); // 在子类静态方法中,super对象指向父类本身,调用父类静态方法 } myMethod(msg) { super.myMethod(msg); // 在子类普通方法中,super对象指向父类原型对象,调用父类方法 } } // 调用子类静态方法 Child.hello(); // hello world Child.myMethod(1); // static 1 // 调用子类普通方法 var child = new Child(); child.myMethod(2); // instance 2
Decorator 修饰器
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
只能用于修饰类和类属性(eg:不能用于函数,因为存在函数提升),本质即编译时执行的函数 (不是在运行时)
- 类修饰器
- 参数(只有一个):
target
:所要修饰的目标类(即类本身)
- 参数(只有一个):
- 类属性修饰器
- 参数(三个):
target
: 类的原型对象(类.prototype)-- 修饰器的本意是要“修饰”类的实例,但是这个时候实例还没生成,所以只能去修饰原型name
: 修饰的类属性名decriptor
:修饰类属性的描述对象
- 返回:该属性的描述对象
Descriptor
- 参数(三个):
- 修饰器参数扩展:
- 可以通过在修饰器外面再封装一层函数来传入入其他参数
- 多个修饰器:
- 会像剥洋葱一样,先从外到内进入,然后由内向外执行
- 第三方模块提供的修饰器:
core-decorators.js
:- @autobind : 使得方法中的this对象,绑定原始对象
- @readonly : 使得属性或方法不可写
- @override : 检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错
- @deprecate/@deprecated : 在控制台显示一条警告,表示该方法将废除
- @suppressWarnings : 抑制deprecated修饰器导致的console.warn()调用 (异步代码发出的调用除外)
traits-decorator
:- @traits : 效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等
示例:
类修饰器
@testable class MyTestableClass { // ... } function testable(target) { // target为所要修饰的目标类(类本身),即MyTestClass target.isTestable = true; // 为类MyTestClass添加静态属性isTestable,想添加实例属性,可以通过目标类的prototype对象操作 } MyTestableClass.isTestable // true
类属性修饰器
function readonly(target, name, descriptor){ descriptor.writable = false; return descriptor; } // descriptor对象原来的值如下 // { // value: specifiedFunction, // enumerable: false, // configurable: true, // writable: true // }; class Person { @readonly // readonly(Person.prototype, 'name', descriptor); name() { return `${this.first} ${this.last}` } }
class Math { @log add(a, b) { return a + b; } } function log(target, name, descriptor) { var oldValue = descriptor.value; descriptor.value = function() { console.log(`Calling ${name} with`, arguments); return oldValue.apply(this, arguments); }; return descriptor; } const math = new Math(); math.add(2, 4); // Calling add with 2,4
多个修饰器
function dec(id){ console.log('evaluated', id); return (target, property, descriptor) => console.log('executed', id); } class Example { @dec(1) @dec(2) method(){} } const example=new Example(); example.method(); // evaluated 1 // evaluated 2 // executed 2 // executed 1
应用: 实现Mixin模式(在一个对象之中混入另外一个对象的方法)
使用类修饰器实现
// mixins.js export function mixins(...list) { // 可以在修饰器外面再封装一层函数,以便传入其他参数 return function (target) { Object.assign(target.prototype, ...list) // 添加实例属性 } } // main.js import { mixins } from './mixins' const Foo = { foo() { console.log('foo') } }; // 在MyClass类上面“混入”Foo对象的foo方法 @mixins(Foo) class MyClass {} let obj = new MyClass(); obj.foo() // 'foo'
通过类的继承实现 Mixin (上面的方法会改写MyClass类的prototype对象)
// 返回一个继承superclass的子类,该子类包含一个foo方法 let MyMixin = (superclass) => class extends superclass { foo() { console.log('foo from MyMixin'); } }; class MyClass extends MyMixin(MyBaseClass) {} let c = new MyClass(); c.foo(); // "foo from MyMixin"
应用: React 与 Redux 库结合使用
class MyReactComponent extends React.Component {} export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent); // 有了装饰器,就可以写成如下方式: @connect(mapStateToProps, mapDispatchToProps) export default class MyReactComponent extends React.Component {}
模块 Module
模块加载方案:
- CommonJS:用于服务器,动态加载(运行时加载)
- AMD:用于浏览器,动态加载(运行时加载)
- ES6:浏览器和服务器通用,静态加载(编译时加载)
动态加载 vs 静态加载:
- 动态加载:
- 运行时加载,无法在编译时做“静态优化”
- 模块输出的是值的拷贝,不存在动态更新
- 静态加载:
- 编译时加载,编译时就能确定模块的依赖关系,以及输入和输出的变量;
- 模块输出的是值的引用(类似Unix 系统的“符号连接”),可动态更新
ES6 Module
- 自动采用严格模式,不管有没有在模块头部加上"use strict" (ES5引入的)
- 一个模块就是一个独立的文件,该文件内部的内容,外部无法获取
- 导出/导入
export ...
:定义模块的对外接口,可处于模块顶层的任何位置- 一个module可以有多条export,但只能有一条
export default
export default
:- 指定默认输出,这样 import 时就可以指定一个任意名字给加载项
- 本质上,就是输出一个叫做default的变量或方法 ( 将default后面的值,赋给default变量 ),然后系统允许你为它取任意名字
- 一个module可以有多条export,但只能有一条
import ... from ...
:加载模块,Singleton 模式,静态加载(在静态解析阶段执行,所以它是一个模块之中最早执行的)- 静态执行,不能使用表达式和变量,不能使用逻辑判断动态加载
- 输入的加载项是只读的(本质是输入接口),即不允许在加载模块的脚本里面改写接口
- 多次重复执行同一句import语句或多次加载同一module,也只会执行一次
import <module>
: 不导入任何值,仅仅执行所加载的模块,eg: import 'lodash';
export ... from ...
:- 在一个模块之中,输入输出同一个模块,export 和 import 可复合成一条
- 注:
export * from 'xxx'
会忽略export default输出
- 可使用
as
重命名加载对象
- 注:
- 模块之中,顶层的this指向undefined(CommonJS 模块的顶层this指向当前模块)
- 通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面 -- 但不建议这样使用
示例:
export...
export 变量
// 写法一 export var m = 1; // 写法二 var m = 1; export {m}; // 写法三 var m = 1; export {m as n};
export 方法
// 写法一 export function f() {}; //写法二 function f() {} export {f}; // 写法三 function f() {} export {f as fun};
- export 多个,重命名
export {m,f} export {m as n, f as fun, f as fun2}
import...from...
- cicle.js (module export):
// circle.js export function area(radius) { return Math.PI * radius * radius; } export function circumference(radius) { return 2 * Math.PI * radius; }
import 部分:
import { area, circumference } from './circle'; console.log('圆面积:' + area(4)); console.log('圆周长:' + circumference(14)); // 输入的变量都是只读的,因为它的本质是输入接口,不允许在加载模块的脚本里面,改写接口 area.foo = 'hello'; // 合法操作 area = {}; // Syntax Error : 'area' is read-only;
import 所有(整体加载):
import * as circle from './circle'; console.log('圆面积:' + circle.area(4)); console.log('圆周长:' + circle.circumference(14)); // 整体加载所在的那个对象,应该是可以静态分析的,不允许运行时改变 circle.foo = 'hello'; // Syntax Error circle.area = function () {}; // Syntax Error
注:
- 以上加载部分和整体加载的区别
import是静态执行,不能使用表达式和变量
// 报错 import { 'f' + 'oo' } from 'my_module'; // 报错 let module = 'my_module'; import { foo } from module; // 报错 if (x === 1) { import { foo } from 'module1'; } else { import { foo } from 'module2'; }
多次重复执行同一句import语句或多次加载同一module,也只会执行一次
import 'lodash'; import 'lodash'; // 只执行一次,等同于:import 'lodash'; import { foo } from 'my_module'; import { bar } from 'my_module'; // 只执行一次,等同于:import { foo, bar } from 'my_module';
- cicle.js (module export):
export default ...
export和export default混合
export default function (obj) { ···} export function each(obj, iterator, context) {···} export { each as forEach }; import _ , { each, forEach } from 'lodash'; // _ 即代表export default的内容
export和export default比较
// 第一组:export export function crc32() { ...}; import {crc32} from 'crc32'; // import 使用大括号 {} // 第二组:export default export default function crc32() { ...} // 同 export default function () { ... } import a from 'crc32'; // import 不使用大括号{},且可直接指定一个任意的名字
export default 变量(export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句)
var a = 1; export default a; // 正确 export default var a = 1; // 错误 export default 42; // 正确 export 42; // 错误
export ... from ...
部分导入导出:
export {...} from ...
export { foo, bar } from 'my_module'; // foo和bar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口 // 可以简单理解为 import { foo, bar } from 'my_module'; export { foo, bar };
整体导入导出:
export * from ...
( 注:会忽略模块的default方法)// 整体输出 export * from 'my_module'; // 会忽略模块的default方法 //默认接口的写法 export { default } from 'my_module'; export { default as es6 } from './my_module';
浏览器环境加载
使用type="module"
- 默认异步加载
- 按在页面出现的顺序依次执行加载
- 模块:
- 代码是在模块作用域之中运行,而不是在全局作用域运行
- 可以使用
import
命令加载其他模块,.js
后缀不可省略 - 顶层的this关键字返回undefined,而不是指向window,利用这个特点,可以侦测当前代码是否在 ES6 模块之中(eg:
const isNotModuleScript = this !== undefined;
)
- 转码:若浏览器不支持 ES6 Module,可以将其转为 ES5 的写法,eg:使用Babal,SystemJS
示例:
浏览器加载javascript(使用
type="application/javascript"
默认语言,可省略)// 默认同步加载 <script src="path/to/myModule.js"></script> // 异步加载 // defer: 渲染完再执行;多个defer时,会按照它们在页面出现的顺序加载; // async:下载完就执行,会中断渲染,执行完成后恢复渲染; 多个async时,不保证加载顺序; <script src="path/to/myModule.js" defer></script> <script src="path/to/myModule.js" async></script>
浏览器加载ES6 Module
<script type="module" src="./foo.js"></script> <!-- 等同于 --> <script type="module" src="./foo.js" defer></script>
Node环境加载
CommonJS Module VS ES6 Module:
操作 | CommonJS Module | ES6 Module |
---|---|---|
输出 | 值的拷贝,不存在动态更新 | 值的引用,可动态更新 |
加载 | 运行时加载,无法做“静态优化” (加载的是一个对象,即module.exports属性,该对象只有在脚本运行完才会生成) |
编译时加载,可做“静态优化” (只是一种静态定义,编译时就能确定模块的依赖关系,以及输入和输出的变量) |
循环加载 | 使用require命令加载脚本 ( 返回的是当前已经执行的部分的值,而不是代码全部执行后的值) |
使用import命令加载 ( 返回的是引用,需要开发者自己保证,真正取值的时候能够取到值) |
第一次加载: 会执行整个脚本,在内存中生成一个对象; 第N次加载:不会再执行,直接到缓存中取值,除非手动清除缓存 |
加载项不会被缓存,已加载项不会重复加载 |
注:
CommonJS Module第一次加载在内存中生成的对象如下:
{ id: '...', // 模块名 exports: { ... }, // 模块输出的各个接口(以后需要用到这个模块的时候,就会到exports属性上面取值) loaded: true, // 表示该模块的脚本是否执行完毕 ... }
CommonJS Module 循环加载示例:(加载返回的是当前已经执行的部分的值,而不是代码全部执行后的值)
- a.js : 加载 b.js
exports.done = false; var b = require('./b.js'); console.log('在 a.js 之中,b.done = %j', b.done); exports.done = true; console.log('a.js 执行完毕');
- b.js : 加载 a.js
exports.done = false; var a = require('./a.js'); console.log('在 b.js 之中,a.done = %j', a.done); exports.done = true; console.log('b.js 执行完毕');
- main.js : 加载 a.js,b.js
var a = require('./a.js'); var b = require('./b.js'); console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
node执行
$ node main.js 在 b.js 之中,a.done = false b.js 执行完毕 在 a.js 之中,b.done = true a.js 执行完毕 在 main.js 之中, a.done=true, b.done=true
- a.js : 加载 b.js
Node 模板加载方案:
Node 有自己的 CommonJS 模块格式,与 ES6 模块格式不兼容,所以ES6 模块和 CommonJS 需采用各自的加载方案
- ES6
- 采用
.mjs
后缀文件名 - 使用
export
/import
命令,不能使用require
命令
- 采用
- CommonJS
- 使用
module.export
/require
命令
- 使用
- ES6 module 加载 CommonJS module:
- 使用
import ... from ...
命令 - Node 会将
module.exports
属性,当作模块的默认输出,即等同于export default ...
- 使用
- CommonJS module 加载 ES6 module:
- 使用
import(...)
函数 - ES6 模块的所有输出接口,会成为输入对象的属性(注:不能使用require命令)
- 使用
- 注:
- 通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但不建议这样使用
- ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量
- ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块
- 在 ES6 模块之中不存在的顶层变量:arguments,require,module,exports,filename,dirname
说明:
import ... from ...
命令:异步加载,只支持加载本地模块,不支持加载远程模块- 模块名不含路径:会去node_modules目录寻找这个模块
- 模块名包含路径:会按照路径去寻找这个名字的脚本文件
- 省略了后缀名的加载,依次尝试:
- 依次尝试四个后缀名:mjs,js,json,node;
- 尝试加载该目录下的package.json的main字段指定的脚本;
- 尝试加载该目录下的名为index,后缀为mjs,js,json,node的文件
import(...)
函数:同步加载- 返回一个Promise对象,实现动态加载,类似于 Node 的require方法(异步加载)
- 与所加载的模块没有静态连接关系,可以用在任何地方,非模块的脚本也可以使用
示例:
ES6 Module加载CommonJS Module : 使用
import...from...
命令a.js (CommonJS module): CommonJS模块的输出都定义在module.exports这个属性上面
module.exports = { foo: 'hello', bar: 'world' }; // 等同于 ES6: export default { foo: 'hello', bar: 'world' };
ES6 Module导入a.js (注:需使用整体输入):使用Node的
import...from...
命令加载 CommonJS 模块,Node 会自动将module.exports
属性,当作模块的默认输出,即等同于export default ...
// 写法一 import baz from './a'; // baz = {foo: 'hello', bar: 'world'}; // 写法二 import {default as baz} from './a'; // baz = {foo: 'hello', bar: 'world'}; // 写法三 import * as baz from './a'; // baz = { // get default() {return module.exports;}, // get foo() {return this.default.foo}.bind(baz), // get bar() {return this.default.bar}.bind(baz) // } baz.default // {foo: 'hello', bar: 'world'} baz.foo // hello // 注意:ES6 模块是编译时确定输出接口,CommonJS 模块是运行时确定输出接口,只有在运行时才能确定foo,所以下面方式不正确 import { foo } from './a'
CommonJS module加载ES6 module: 使用
import(...)
函数(注:不能使用require命令),ES6 模块的所有输出接口,会成为输入对象的属性- ES6 Module:es.js
export let foo = { bar:'my-default' }; export { foo as bar }; export function f() {}; export class c {};
- CommonJS 导入 es.js
const es_namespace = await import('./es'); // es_namespace = { // get foo() {return foo;} // get bar() {return foo;} // get f() {return f;} // get c() {return c;} // }
- ES6 Module:es.js
实践
var/let/const
let取代var
- 两者语义相同,且let没有副作用:var命令存在变量提升效用,let命令没有这个问题,let只在其声明的代码块内有效
优先使用const
- 防止了无意间修改变量值所导致的错误(函数应该都设置为const)
- const比较符合函数式编程思想,运算不改变值,只是新建值,这样也有利于将来的分布式运算;
- JavaScript编译器会对const进行优化,有利于提高程序的运行效率(let和const的本质区别,其实是编译器内部的处理不同)
- 长远来看,JavaScript 可能会有多线程的实现,const利于保证线程安全
解构赋值
以下情况优先使用解构赋值
使用数组成员对变量赋值
const arr = [1, 2, 3, 4]; // bad const first = arr[0]; const second = arr[1]; // good const [first, second] = arr;
函数的参数是对象的成员
// bad function getFullName(user) { const firstName = user.firstName; const lastName = user.lastName; } // good function getFullName(obj) { const { firstName, lastName } = obj; } // best function getFullName({ firstName, lastName }) { }
函数返回多个值,优先使用对象的解构赋值(注:不是数组的解构赋值),便于以后添加返回值,以及更改返回值的顺序
// bad function processInput(input) { return [left, right, top, bottom]; } // good function processInput(input) { return { left, right, top, bottom }; } const { left, right } = processInput(input);
String
- 静态字符串: 使用单引号或反引号
- 动态字符串: 使用反引号
// bad
const a = "foobar";
const b = 'foo' + a + 'bar';
// acceptable
const c = `foobar`;
// good
const a = 'foobar';
const b = `foo${a}bar`;
Array
拷贝数组: 使用扩展运算符
...
// bad const len = items.length; const itemsCopy = []; let i; for (i = 0; i < len; i++) { itemsCopy[i] = items[i]; } // good const itemsCopy = [...items];
对象转为数组: 使用
Array.from
方法const foo = document.querySelectorAll('.foo'); const nodes = Array.from(foo);
Object
定义对象
- 单行定义:最后一个成员不以逗号结尾
多行定义:最后一个成员可以逗号结尾
// bad const a = { k1: v1, k2: v2, }; const b = { k1: v1, k2: v2 }; // good const a = { k1: v1, k2: v2 }; const b = { k1: v1, k2: v2, };
尽量静态化
- 定义后尽量不添加新的属性
使用
Object.assign
方法添加属性// bad const a = {}; a.x = 3; // if reshape unavoidable const a = {}; Object.assign(a, { x: 3 }); // good const a = { x: null }; a.x = 3;
- 动态属性名: 可在创造对象的时候,用属性表达式定义
// bad const obj = { id: 5, name: 'San Francisco', }; obj[getKey('enabled')] = true;
// good const obj = { id: 5, name: 'San Francisco' ,[getKey('enabled')]: true, };
尽量简洁表达属性和方法(易于描述和书写)
var ref = 'some value'; // bad const atom = { ref: ref, value: 1, addValue: function (value) { return atom.value + value; }, }; // good const atom = { ref, value: 1, addValue(value) { return atom.value + value; }, };
Function
建议尽量使用箭头函数的情况:
- 立即执行函数
(() => { console.log('Welcome to the Internet.'); })();
原来一些需要使用函数表达式的场合
// bad [1, 2, 3].map(function (x) { return x * x; }); // good [1, 2, 3].map((x) => { return x * x; }); // best [1, 2, 3].map(x => x * x);
取代Function.prototype.bind(不再用 self/_this/that 绑定 this)
// bad const self = this; const boundMethod = function(...params) { return method.apply(self, params); } // acceptable const boundMethod = method.bind(this); // best const boundMethod = (...params) => method.apply(this, params);
- 注:简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法
- 立即执行函数
函数参数
使用rest运算符
...
代替arguments
变量 (arguments 是一个类似数组的对象,而rest运算符可以提供一个真正的数组)// bad function concatenateAll() { const args = Array.prototype.slice.call(arguments); return args.join(''); } // good function concatenateAll(...args) { return args.join(''); }
使用默认值语法设置参数的默认值
// bad function handleThings(opts) { opts = opts || {}; } // good function handleThings(opts = {}) { // ... }
Class
用Class取代需要 prototype的操作(Class语法更简洁易理解)
// bad function Queue(contents = []) { this._queue = [...contents]; } Queue.prototype.pop = function() { const value = this._queue[0]; this._queue.splice(0, 1); return value; } // good class Queue { constructor(contents = []) { this._queue = [...contents]; } pop() { const value = this._queue[0]; this._queue.splice(0, 1); return value; } }
用extends实现继承(extend语法更简单,且不会有破坏instanceof运算的危险)
// bad const inherits = require('inherits'); function PeekableQueue(contents) { Queue.apply(this, contents); } inherits(PeekableQueue, Queue); PeekableQueue.prototype.peek = function() { return this._queue[0]; } // good class PeekableQueue extends Queue { peek() { return this._queue[0]; } }
Module
使用import取代require
// bad const moduleA = require('moduleA'); const func1 = moduleA.func1; const func2 = moduleA.func2; // good import { func1, func2 } from 'moduleA';
使用export取代module.exports
// commonJS的写法 var React = require('react'); var Breadcrumbs = React.createClass({ render() { return <nav />; } }); module.exports = Breadcrumbs; // ES6的写法 import React from 'react'; class Breadcrumbs extends React.Component { render() { return <nav />; } }; export default Breadcrumbs;
尽量不使用通配符来确保至少有一个默认输出(export default)
// bad import * as myObject from './importModule'; // good import myObject from './importModule';
ESLint
一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码
安装
// 安装ESLint $ npm i -g eslint //安装 Airbnb 语法规则 $ npm i -g eslint-config-airbnb //安装import、a11y、react 插件 $ npm i -g eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
配置(项目的根目录下新建一个.eslintrc文件)
{ "extends": "eslint-config-airbnb" }
使用,eg:
index.js
var unusued = 'I have no purpose!'; function greet() { var message = 'Hello, World!'; alert(message); } greet();
使用 ESLint 检查index.js,发现错误
$ eslint index.js index.js 1:1 error Unexpected var, use let or const instead no-var 1:5 error unusued is defined but never used no-unused-vars 4:5 error Expected indentation of 2 characters but found 4 indent 4:5 error Unexpected var, use let or const instead no-var 5:5 error Expected indentation of 2 characters but found 4 indent × 5 problems (5 errors, 0 warnings)
- 不应该使用var命令,而要使用let或const
- 定义了变量,却没有使用
- 行首缩进为 4 个空格,而不是规定的 2 个空格