2. let 和 const 命令
1. let
暂时性死区
这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。
- 变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。
typeof x; // ReferenceError
x = 'abc'; // ReferenceError
let x;
- 作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。
typeof undeclared_variable // "undefined"
- 有些“死区”比较隐蔽,不太容易发现。
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
上面代码中,调用bar函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于”死区“。如果y的默认值是x,就不会报错,因为此时x已经声明了
- 下面代码报错,也是因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“。
/ 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
2. 块级作用域
ES6 的块级作用域
块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
3. const
const 本质
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。 因此,将一个对象声明为常量必须非常小心。
如果真的想将对象冻结,应该使用Object.freeze方法。
const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
上面代码中,常量foo指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。
除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。
const people = {
name: '菜菜',
age: 25,
family: [{ name: '妈妈', age: 50 }, { name: '哥哥', age: 30 }],
like: {
name: 'guitar',
type: 'music',
from: { country: 'China' }
}
};
function handle(obj) {
Object.freeze(obj);
Object.keys(obj).map(key => {
if (typeof obj[key] === 'object') {
handle(obj[key]);
}
});
}
handle(people);
people.name = '111'; // 会报错
console.log(people.name);
;
};
ES6 声明变量的六种方法
ES5 只有两种声明变量的方法:var命令和function命令。ES6 除了添加let和const命令,后面章节还会提到,另外两种声明变量的方法:import命令和class命令。所以,ES6 一共有 6 种声明变量的方法。
4. 顶层对象的属性
顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。
window.a = 1;
a // 1
a = 2;
window.a // 2
ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
上面代码中,全局变量a由var命令声明,所以它是顶层对象的属性;全局变量b由let命令声明,所以它不是顶层对象的属性,返回undefined。
5. global 对象
同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。
- 全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。
- 函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined。
可以看下原文的一般判断方法。
现在有一个提案,在语言标准的层面,引入global作为顶层对象。也就是说,在所有环境下,global都是存在的,都可以从它拿到顶层对象。
垫片库system.global模拟了这个提案,可以在所有环境拿到global。
// CommonJS 的写法
var global = require('system.global')();
// ES6 模块的写法
import getGlobal from 'system.global';
const global = getGlobal();
上面代码将顶层对象放入变量global。
Class
1. 简介
ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
本质上,ES6 的类只是 ES5 的构造函数的一层包装。
ES5的构造函数写法:
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
ES6的类写法:
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
doStuff() {
console.log('stuff');
}
}
typeof Point // "function" 表明类的数据类型就是函数
Point === Point.prototype.constructor // true 表明类本身就指向构造函数
var b = new Point();
b.doStuff() // "stuff"
// 等同于
Point.prototype = {
constructor() {},
toString() {},
doStuff() {},
};
- ES5 的构造函数Point,对应 ES6 的Point类的构造方法constructor。
- 类的所有方法都定义在类的prototype属性上面。
注意
- 定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。
- 方法之间不需要逗号分隔,加了会报错。
class B {}
let b = new B();
b.constructor === B.prototype.constructor // true
我的理解
ES5:实例b的constructor(构造函数)属性指向构造函数B。(可以看出这里还是相同的)
prototype对象的constructor属性,直接指向“类”的本身,这与 ES5 的行为是一致的。如下:
Point.prototype.constructor === Point // true
类的内部所有定义的方法,都是不可枚举的(non-enumerable)【这一点与 ES5 的行为不一致。】
Object.keys(Point.prototype)// []
Object.getOwnPropertyNames(Point.prototype)// ["constructor","toString","doStuff"]
2. constructor方法
constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
注意点
(1)严格模式
类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。
(2)不存在提升
类不存在变量提升(hoist),这一点与 ES5 完全不同(我的描述:es5中函数声明、变量声明都会被提升到所在作用域的顶部)。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。
(3)name 属性
由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。
// es5
function Person(){};
Person.name // 'Person'
// es6
class Point {}
Point.name // "Point"
name属性总是返回紧跟在class关键字后面的类名。