这是我参与8月更文挑战的第6天,活动详情查看:8月更文挑战
let和const
暂时性死区
因为let的出现,typeof不再安全,之前对声明未赋值的变量使用typeof会得到undefined,但使用let会形成
封闭作用域,如果变量声明未赋值使用typeof会报引用错误
好处(使用es6的let和const替换es5中的var)
可以避免以下两类错误
- 变量在声明前被使用
- 在相同作用域声明同一变量
为什么需要块级作用域
- 由于使用var声明的变量存在变量提升(变量可以在声明之前使用,值为undefined)会导致函数作用域的变量覆盖全局作用域的变量
- 循环体中声明的计数变量在循环结束后泄露成全局变量
es6诞生的块级作用域
使用let形成的封闭作用域相当于增加了块级作用域
块级作用域作用
可以取代立即执行函数表达式(IIFE)
const特点
- 一旦声明,值不能被改变(本质上为保证变量所指向的内存地址不变)
- 对于简单数据类型,值就保存在变量指向的内存地址(栈中)
- 对于引用数据类型,变量保存的是指向某个内存地址(堆中)的指针,const只能保证内存地址不变,但内存地址所保存的数据可变
- 由于第一点,所以必须在声明的时候初始化,否则会报错
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中顶层对象的属性与全局变量等价
弊端如下:
- 编译阶段无法报出变量未声明的错误,只有运行时才能发现(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的)
- 误操作产生的全局变量导致顶层对象的属性可以导出读写,不利于模块化编程的实现
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的属性,解构赋值可以取到这个属性