ES6特性解析

384 阅读6分钟

这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战

let和const

暂时性死区

因为let的出现,typeof不再安全,之前对声明未赋值的变量使用typeof会得到undefined,但使用let会形成封闭作用域,如果变量声明未赋值使用typeof会报引用错误

好处(使用es6的let和const替换es5中的var)

可以避免以下两类错误

  1. 变量在声明前被使用
  2. 在相同作用域声明同一变量

为什么需要块级作用域

  1. 由于使用var声明的变量存在变量提升(变量可以在声明之前使用,值为undefined)会导致函数作用域的变量覆盖全局作用域的变量
  2. 循环体中声明的计数变量在循环结束后泄露成全局变量

es6诞生的块级作用域

使用let形成的封闭作用域相当于增加了块级作用域

块级作用域作用

可以取代立即执行函数表达式(IIFE)

const特点

  1. 一旦声明,值不能被改变(本质上为保证变量所指向的内存地址不变)
  2. 对于简单数据类型,值就保存在变量指向的内存地址(栈中)
  3. 对于引用数据类型,变量保存的是指向某个内存地址(堆中)的指针,const只能保证内存地址不变,但内存地址所保存的数据可变
  4. 由于第一点,所以必须在声明的时候初始化,否则会报错

const存储引用变量举例

const foo = {};

// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
const a = [];
a.push('Hello'); // 可执行
a.length = 0;    // 可执行

使用const声明冻结对象

冻结对象

const foo = Object.freeze({});

// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;

冻结对象及其对象属性(递归冻结)

var constantize = (obj) => {
  Object.freeze(obj);
  Object.keys(obj).forEach( (key, i) => {
    if ( typeof obj[key] === 'object' ) {
      constantize( obj[key] );
    }
  });
};

ES6变量声明方式

  • var(原ES5)
  • function(原ES5)
  • let
  • const
  • import
  • class

顶层对象的属性

ES5中顶层对象的属性与全局变量等价

弊端如下:

  1. 编译阶段无法报出变量未声明的错误,只有运行时才能发现(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的)
  2. 误操作产生的全局变量导致顶层对象的属性可以导出读写,不利于模块化编程的实现

ES6中使用新的四种方式声明的变量不会成为顶层对象的属性

var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1

let b = 1;
window.b // undefined

任意环境取到顶层对象

// 方法一
(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');
};

Symbol

特点

是ES第七种数据类型

出现原因

对象的属性都是字符串,容易造成属性名冲突,引入Symbol可以保证对象的属性名独一无二

声明Symbol变量

  • 不能使用new调用Symbol函数,而是直接调用——见例1
  • 调用Symbol时可以传入参数,用以自定义标识——见例2
  • 调用Symbol的参数为对象时,会调用该对象的toString方法,然后生成一个Symbol值——见例3
  • Symbol是一种新的特殊数据类型,不能与其他数据类型进行运算——见例4
  • 虽然不能和其他数据类型变量进行运算,但是可以显示转换为字符串或布尔类型,但不能转为数值——见例5

例1

let symb = Symbol()

例2

let s1 = Symbol('s1')
let s2 = Symbol('s2')

s1
Symbol(s1)
s1.toString()
"Symbol(s1)"

s2
Symbol(s2)
s2.toString()
"Symbol(s2)"

let s3 = Symbol('s1')
s3
Symbol(s1)
s1 == s2
false
s1 == s3
false

例3

const obj3 = {
	toString() {
		return 'abc'
	}
}
const sym3 = Symbol(obj3)
console.log(sym3)
Symbol(abc)

const obj4 = {
	name: 'obj4'
}
const sym4 = Symbol(obj4)
console.log(sym4)
Symbol([object Object])

例4

const str5 = 'I am a string';
const sym5 = Symbol('sym5')
const result5 = str5 + sym5;
console.log(result5);

Uncaught TypeError: Cannot convert a Symbol value to a string

class(类)

本质

  • 对象模板
  • 相当于语法糖(大多功能ES5也可以做到)

解读

  • ES5中的构造函数对应ES6中的构造方法
  • 类的本质还是函数——见例1
  • 类本身指向构造函数——见例1
  • 类的所有方法都定义在类的prototype属性上面(所以可以通过在prototype对象上添加方法给类添加新方法)——见例2、3
  • 类内部定义的方法都是不可枚举的(但ES5中函数内部定义的函数是可枚举的)——见例4、5
  • 类必须使用new关键字调用,和ES5的函数不同(可以直接执行)
  • ES5&ES6实例的属性除非显式定义在本身上(定义在this对象上),否则都定义在原型上(class上)——见例6
  • ES5&ES6所有实例共享同一个原型对象
  • 通过对实例的原型对象(proto)(proto 依赖于环境,生产中使用Object.getPrototypeOf获取实例对象原型)增加方法,可以为类添加方法(但实际上不推荐,,会改变类的原始定义,影响所有实例)

例1

class Point {
  // ...
}

typeof Point // "function"
Point === Point.prototype.constructor // true

例2

class Point {
  constructor() {
    // ...
  }

  toString() {
    // ...
  }

  toValue() {
    // ...
  }
}

// 等同于

Point.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
};

例3

class Point {
  constructor(){
    // ...
  }
}

Object.assign(Point.prototype, {
  toString(){},
  toValue(){}
});

例4

class Point {
  constructor(x, y) {
    // ...
  }

  toString() {
    // ...
  }
}

Object.keys(Point.prototype)
// []
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]

例5

var Point = function (x, y) {
  // ...
};

Point.prototype.toString = function() {
  // ...
};

Object.keys(Point.prototype)
// ["toString"]
Object.getOwnPropertyNames(Point.prototype)
// ["constructor","toString"]

例6

//定义类
class Point {

  constructor(x, y) {
    this.x = x;
    this.y = y;
  }

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }

}

var point = new Point(2, 3);

point.toString() // (2, 3)

point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true

存值和取值

  • 使用get/set关键字对属性设置存值/取值函数拦截对该属性的存取行为——见例1
  • ES5&ES6存值函数和取值函数是设置在属性的描述对象Descriptor上的——见例2

例1

class MyClass {
  constructor() {
    // ...
  }
  get prop() {
    return 'getter';
  }
  set prop(value) {
    console.log('setter: '+value);
  }
}

let inst = new MyClass();

inst.prop = 123;
// setter: 123

inst.prop
// 'getter'

例2

class CustomHTMLElement {
  constructor(element) {
    this.element = element;
  }

  get html() {
    return this.element.innerHTML;
  }

  set html(value) {
    this.element.innerHTML = value;
  }
}

var descriptor = Object.getOwnPropertyDescriptor(
  CustomHTMLElement.prototype, "html"
);

"get" in descriptor  // true
"set" in descriptor  // true

解构赋值

解构条件

具有Iterator接口的数据结构

反例

// 报错
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};

特例

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

原因:Generator函数原生具有 Iterator 接口

解构赋值默认值

let [x, y = 'b'] = ['a', undefined]; // x='a', y='b'

默认值在不满足严格等于(即===)的情况下才会触发

let [x = 1] = [null];
x // null

默认值为表达式会惰性求值,即

function f() {
  console.log('aaa');
}

let [x = f()] = [1];

等价于

let x;
if ([1][0] === undefined) {
  x = f();
} else {
  x = [1][0];
}

对象的解构赋值可以取到继承的属性

const obj1 = {};
const obj2 = { foo: 'bar' };
Object.setPrototypeOf(obj1, obj2);

const { foo } = obj1;
foo // "bar"

对象obj1的原型对象是obj2。foo属性不是obj1自身的属性,而是继承自obj2的属性,解构赋值可以取到这个属性