《ECMAScript 6 入门》笔记

@一棵菜菜  October 24, 2018

2. let 和 const 命令

1. let

暂时性死区

这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。

  1. 变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。
typeof x; // ReferenceError
x = 'abc'; // ReferenceError
let x;
  1. 作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。
typeof undeclared_variable // "undefined"
  1. 有些“死区”比较隐蔽,不太容易发现。
function bar(x = y, y = 2) {
  return [x, y];
}

bar(); // 报错

上面代码中,调用bar函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于”死区“。如果y的默认值是x,就不会报错,因为此时x已经声明了

  1. 下面代码报错,也是因为暂时性死区。使用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() {},
};
  1. ES5 的构造函数Point,对应 ES6 的Point类的构造方法constructor。
  2. 类的所有方法都定义在类的prototype属性上面。

注意

  1. 定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。
  2. 方法之间不需要逗号分隔,加了会报错。
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关键字后面的类名


添加新评论