let和const
ES6中引入了let和const进行变量申明,一方面可以避免污染全局作用域和变量冲突,另一方面也引入了一个新得作用域——块级作用域;
ES5
for (var i = 0; i < 10; i++) {
continue;
}
console.log(i); // 10
ES6
for (let i = 0; i < 10; i++) {
continue;
}
console.log(i); // i is not defined
块级作用域的引入可以有效解决ES6之前循环变量的引用问题。如:
ES5
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
上例输出为 10 10 10 10 10 10 10 10 10 10,和预期并不一样,通常需要使用立即执行函数以获得正确结果。而ES6中则可以轻松实现;
ES6
for (let i = 0; i < 10; i++) {
setTimeout(function () {
console.log(i); // 输出0-9
}, 1000);
}
需要注意的是:与var不同,let并不存在变量提升,需要先声明后使用;重复声明同一变量会报错;暂时性死区;
// 暂时性死区
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
有意思的是for循环中的循环条件和循环体中可以声明相同的变量名,并且具有各自独立的作用域;
for (let i = 0; i < 2; i++) {
let i = 'I am re-defined.';
console.log(i);
}
const用于声明常变量,声明并赋值后不可以再修改;const必须在声明的同时进行初始化,否则会报错;对于引用类型,其对象属性可以修改,但是对象的引用也就是对应的其在内存中的位置不可以修改;
const obj = {a:1, b:2};
obj.a = 11;
console.log(obj); // OK. the output is {a:11, b:2}
obj = {};
console.log(obj); // Assignment to constant variable
default、rest、spread
function sum (a, b = 2) {
return a + b;
}
sum(1); // 3;
function fn (x, ...y) {
return x + y.length;
}
fn(1, 2, 3); // 3
function fc (x, y, z ) {
return x + y + z;
}
fc(...[1,2,3]); // 6
Symbol
Symbol是ES6引入的一种新的基本类型,它的引入使得对象的属性名可以为string类型或者symbol类型。其可以接受一个字符串参数以区分不同的symbol实例;
每个Symbol是独一无二的,因此可以用作属性名以防止发生命名冲突。但是它们并不是私有的,可以通过Reflect.ownKeys(obj)、Object.getPropertySymbols()等反射特性访问;但是,其不能通过for…in、for…of、Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()等返回;
let s = Symbol();
Object.prototype.toString.call(s) // "[object Symbol]"
Symbol可以通过String(symbolObj)或者symbolInstance.toString()显式转为字符串,但不能转为其他类型或与其他类型进行运算;
在使用Symbol时需要注意:当其作为属性名时,访问需要使用[]访问符而不能是点号访问符;当其作为对象属性时,需要在[]中定义,否则会将该变量名转为字符串当做属性名;
let obj = {
[sum] (a, ...arr) { ... }
}
Symbol有两个方法:Symbol.for()和Symbol.keyFor();给Symbol.for()传入相同的字符串可以得到相同的symbol实例;Symbol.keyFor()与Symbol.for()是相关的,Symbol.for()每次调用会根据是否存在该实例在全局作用域中生成或直接返回一个symbol实例,每次调用后的实例都会注册到全局作用域中(这是与Symbol()不同的,Symbol()即使传入相同的值也会重新创建实例),而Symbol.keyFor()就是用于检测实例是否注册到全局作用域中,即实例是否是通过Symbol.for()方法创建的;
var s1 = Symbol('s');
var s2 = Symbol('s');
s1 === s2; // false
var s1 = Symbol.for('s');
var s2 = Symbol.for('s');
s1 === s2; // true
var s3 = Symbol('s');
Symbol.keyFor(s1); // 's'
Symbol.keyFor(s3); // undefined
Map + WeakMap + Set + WeakSet
ES6新引入的高效数据结构用于支持常用算法;
Map
Map的出现打破了传统的对象属性名只能为字符串的限制,可以使用任何类型作为属性名;Map本身为构造函数,实例化时可以接收一个数组作为参数;其实例具有size属性和set()/ get()/ has()/ delete()/ clear()方法
注意引用类型的差异性,需要引用同一对象(引用同一块内存地址);其实这在一定程度上也解决了对象属性名冲突的问题(即使名称相同,引用的不是同一块内存地址);
var map = new Map()
map.set({},'a');
map.get({}); // undefined; 因为引用的并不是同一对象;
map.set([1], 123);
map.get([1]); // undefined; 因为引用的并不是同一块内存;[1] === [1] //false
另外,Map还提供了一系列的遍历器生成函数和遍历方法:
WeakMap
WeakMap提供了无内存泄漏的以对象作为键名的副表;也就是说它只接收对象作为键名,对作为键名的对象的引用相当于创建了一个副本,不会计入垃圾回收机制的引用计数中,当其他部分对该对象的引用数为零时,WeakMap中以该对象作为键名的键值对都会自动被垃圾回收机制回收,不用手动删除对该对象的引用;具体可以参考vtxf的示例
很容易想到可以将其应用到容易引起内存泄漏的DOM元素操作上;其次,也可以用它创建一些私有属性;下边是来自阮一峰老师的《ECMAScript 6 入门》的例子:
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(3, () => console.log('DONE'));
c.dec()
c.dec()
与Map相比,WeakMap仅可以对象作为键名,不会造成内存泄漏,没有size属性和clear()方法;同时也不支持各种遍历方法;
Set
Set用于创建值唯一的数据结构,可以接收一个数组作为初始化参数;看到唯一,立马可以想到数组去重的新方法又有了~ (当然,其只能去除扁平化的数组重复项)
与Map一样,set也有自己的实例属性和方法,以及遍历方法;
属性:Set.prototype.constructor(默认为Set函数本身)和Set.prototype.size;
方法:add(value)、has(value)、delete(value)、clear();
遍历:与Map一样,keys()、values()、entries()、forEach(),由于Set()结构没有键名,故keys()与values()遍历的结果是一致的;
WeakSet
WeakSet与Set的差别和WeakMap与Map的差别很相似;WeakSet的成员也只能为对象,同样是一种无内存泄漏的结构,会自动进行垃圾回收;不支持clear()方法;没有size属性,不支持遍历;
箭头函数
箭头函数提供了更加简洁 的函数书写方式。一个形参并且函数体语句为单个语句时可以省略function及return关键字;当形参数大于1时使用()包裹,然后使用“=>”连接;函数体语句数大于1时必须使用“{}”包裹,且需要使用关键字return;
var sum (a, b) => a + b;
arr.map(item => item*2);
与普通函数不同,箭头函数中的this是词法作用域的;普通函数的this通常是在运行时绑定到其调用对象,而箭头函数中继承了定义了它们的上下文的this值(意味着箭头函数没有自己的this,其内部的this是借用了外部的函数/对象的this,这也是为什么其不能和new连用,也无法使用bind方法进行this绑定);同时当箭头函数被其他函数包裹时,其共享了父级函数的arguments参数;由于箭头函数中this的特殊性,箭头函数不可以当作构造函数,不能与new关键字连用;
var obj = {
name: 'obj',
greet () {
setTimeout(() => {
console.log(`Hello, ${this.name}`);
}, 1000);
},
say () {
setTimeout(function () {
console.log(`${this.name} wants to tell a story.`);
}, 1000);
},
test: function () {
setTimeout(function () {
console.log(this.name);
}.bind(this), 1000);
}
}
var name = 'global',
fn = obj.greet,
f = obj.say;
fn();
fn.call({name: 'temp'}); // 返回temp;
obj.greet();
f();
obj.say();
obj.test(); // obj
上例中的普通函数,this指向运行时所在的作用域,函数f运行在全局作用域,而obj.say()由于setTimeout其内部是一个普通的匿名函数,setTimeout函数运行在全局作用域,this指向运行时所在的作用域setTimeout函数对象,因此this.name均返回global;
在箭头函数中,由于this始终绑定的是定义时的所在的作用域,fn定义时所在的作用域为全局作用域,fn.call({name: ‘temp’})中this定义时所在的作用域为对象{name: ‘temp’},而obj.greet()定义时所在的作用域为obj对象,因此分别返回global,temp,obj;
模板字符串
模板字符串使用反引号包裹,可以在其中进行换行或者使用${variable}
包含变量并可以自动解析;在创建一段文档片段时非常方便快捷;
var name = 'Neil';
var str = `Hello, ${name}`; // "Hello, Neil"
var s = `How are you?
I'm fine`;
var s2 = `this is a line-'\n'break`;
解构赋值
ES6的解构赋值允许绑定使用模式匹配,从而从其中提取相应的值,支持数组和对象;解构赋值可以使用默认值,当提取值不存在时返回undefined;匹配的是模式,被赋值的是变量。
// 以下实例由于Iterator的原因会报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
// 数组和布尔值的解构赋值,利用其toString()方法
let {toString: s1} = 123;
console.log(s1);
let {toString: s2} = true;
console.log(s2 === Boolean.prototype.toString);
// 字符串解构赋值
console.log("===== String =====")
let [a1, b1, c1, d1, e1] = 'ha~';
console.log(a1, b1, c1, d1, e1);
let {length: lens} = 'hello world';
console.log(lens);
// 数组解构赋值
console.log("===== Array =====")
let [a, [b, [c,d]]] = [1, [2, [3, 4]]];
console.log(a, b, c, d);
let [x, ,y] = [1, 2, 3];
console.log(x, y);
let [head, ...tail] = [1, 2, 3];
console.log(head, tail);
let [m, n] = [1];
console.log(m, n);
let [p, q = 2] = [1];
console.log(p, q);
let {length: len} = [1, 2, 3, 4, 5];
console.log(len);
// 函数中的解构赋值
console.log("===== Function =====")
function fn({name: s}) {
console.log(s);
}
fn({name: 'Neil'});
function rectArea({x, y, z = 5}) {
console.log(x * y * z);
}
rectArea({x: 3, y: 4})
// 对象的解构赋值
console.log("===== Object =====")
let {name} = {name: 'Neil'};
console.log(name);
let age;
({age} = {age: 3}); // 为避免{}被理解为代码块,需要外边包裹'()'作为表达式
console.log(age);
解构赋值的用途:交换两个变量的值,从返回值为数组或对象的函数的返回值中直接提取多个值,提取JSON数据,从模块中提取指定方法;
Class
class是ES6的一个语法糖,可以提供类似于其他语言的基于原型链继承的面向对象的编程模式,比直接使用原型链继承更加直观方便;支持基于原型的继承,构造器,实例,静态方法和super调用;class不支持变量提升;
下例中this指向实例对象,每个类通过constructor关键字定义类的构造方法,其他类方法则在外部定义;constructor默认返回的是类的实例对象this,也可以修改为返回其他对象;子类的继承是通过在子类的constructor构造方法中通过调用super()方法获得继承自父类的实例对象,然后在此基础上通过this访问父类的属性或者方法并进行修改,以满足子类的构造需求;子类的方法则在constructor外部进行定义;
由输出可以看出,子类是继承了父类的实例对象并在其基础上修改属性和方法得到的;可以看到,构造函数和其他外部函数其实都是定义在原型对象上,而不是每个实例的私有属性和方法;
class Animal {
constructor () {
this.type = 'animal'
}
greet (say) {
console.log(`greet from ${this.type}: ${say}.`);
}
protoMethod () {
console.log(`A method from Animal.`);
}
}
class Dog extends Animal{
constructor () {
super();
console.log(this); // Dog { type: 'animal' }
this.type = 'dog';
console.log(this); // Dog { type: 'dog' }
this.greet = () => console.log('hello');
console.log(this); // Dog { type: 'dog', greet: [Function] }
}
habit () {
console.log('dogs going rattings.');
}
}
let dog = new Dog();
dog.greet('hello');
dog.habit();
dog.protoMethod();
console.log(Animal.prototype.constructor === dog.constructor); // false
console.log(Dog.prototype.greet === dog.greet); // false
console.log(Animal.prototype.greet === dog.greet); // false
console.log(Dog.prototype.habit === dog.habit); // true
console.log(Dog.prototype.protoMethod === dog.protoMethod); // true
私有属性和私有方法
ES6本身并没有提供私有化属性或者方法的统一标准,但是可以借助Symbol的唯一性结合getter方法实现类的私有属性;同样的方式也可以用于定义私有方法;
var Animal = (function () {
const _capability = Symbol();
const _privateMethod = Symbol();
class Animal {
constructor (capability) {
this.type = 'animal';
this[_capability] = capability;
}
get capability () {
return this[_capability];
}
greet (say) {
let personal = this[_privateMethod]();
console.log(`greet from ${this.type}: ${say} I can ${personal}`);
}
protoMethod () {
console.log(`A method from Animal.`);
}
[_privateMethod] () {
return this[_capability];
}
}
return Animal;
})();
class Dog extends Animal{
constructor () {
super();
this.type = 'dog';
}
habit () {
console.log('dogs going rattings.');
}
}
var animal = new Animal('eat'); // eat
animal.capability = 'drink'; // undefined
console.log(animal.capability); // greet from animal: Hi! I can eat
var dog = new Dog('bark');
console.log(dog.capability);
animal.greet('Hi!');
dog.greet('Hi!'); // greet from dog: Hi! I can undefined
// animal[_privateMethod](); // ReferenceError: _privateMethod is not defined
Module
目前已有的模块化规范有AMD、CMD、CommonJS;而ES6之前JS还没有像其他语言那样方便的语法可以载入外部的其他模块;
CommonJS主要用于服务器端,NodeJS采用的就是CommonJS,每个文件会被当做一个模块,使用exports和require关键字进行导出和加载,但由于其是同步加载,故不适用于客户端;
AMD(asynchronous module definition)是一个异步模块加载规范,requireJS、Dogo Toolkit是基于该规范实现的,是一种运行时加载的 规范(在运行时确定模块的依赖关系,关键词有define和require([module], callback),第一个参数为数组,里边是需要加载的模块名称,第二个参数为模块加载完成后的回调函数)。AMD可以作为CommonJS模块的传输格式,只要CommonJS模块不使用计算的同步请求require(“)调用。使用计算同步请求require(“)代码的CommonJS代码可以转换为使用大多数AMD加载器支持的回调样式。
CMD是在Sea.js推广过程中产生的一种规范;它与AMD很相近,但是在依赖的加载上有些区别,AMD规范中依赖需要在文件头部全部加载,而CMD则可以在使用过程中就近加载;感觉有点类似于其他语言中的变量 需要先声明后使用和JS中的变量允许在需要的时候创建;
AMD和CMD是运行时加载,而
参考文献