ECMAScript 6 入门学习笔记

本文是基于阮一峰《ECMAScript 6 入门》做的个人学习笔记,方便今后查阅,所以以下内容基本上绝大部分出自阮一峰的博客。

笔记1:数据类型和语法块

简单说,ECMAScript 是 JavaScript 语言的国际标准,JavaScript 是 ECMAScript 的实现。

let 命令

ES6 新增了 let 命令,用了声明变量,使用它声明的变量只在所在的代码块中有效

{
    let a = 10;
    var b = 11;
}

console.log(a);  //Error  is not defined
console.log(b);  // 11

for 循环的计数器就很合适使用 let 命令。

for(let i = 0; i < 10; i++){
    //...
}
console.log(i);  //Error

而且 let 不存在变量提升(变量可以在声明之前使用),也就是说我们一般需要先使用 let 来声明变量才能使用。

//var
console.log(foo);  //undefined
var foo = 2;

//let
console.log(bar);  //Error bar is not defined
let bar = 3;

ES5 中只有全局作用域和函数作用域,没有块级作用域,let 变量实际上为 JS 新增了块级作用域,而且比较特殊的是 ES6 允许块级作用域的任意嵌套

{{{{
  {
    let insane = 'Hello World'
  }
  console.log(insane); // 报错
}}}};

ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。

// 第一种写法,报错
if (true) let x = 1;

// 第二种写法,不报错
if (true) {
  let x = 1;
}

另外,块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。

// IIFE 写法
(function () {
  var tmp = ...;
  ...
}());

// 块级作用域写法
{
  let tmp = ...;
  ...
}

const 命令

const 声明一个只读的常量。一旦声明,常量的值就不能改变。

const PI = 3.1415;
console.log(PI);

PI = 3;  //Error 

数组的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构。

let [a, b, c] = [1, 2, 3];

解构赋值允许指定默认值:

let [x = 1, y = x] = [];     // x=1; y=1
let [x = 1, y = x] = [2];    // x=2; y=2
let [x = 1, y = x] = [1, 2]; // x=1; y=2
let [x = y, y = 1] = [];     // ReferenceError: y is not defined

对象的解构赋值

解构不仅可以用于数组,还可以用于对象。

let { foo, bar } = { foo: 'aaa', bar: 'bbb' };

对象代理 Proxy

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

var proxy = new Proxy({}, {
  get: function(target, propKey) {
    return 35;
  }
});

proxy.time // 35
proxy.name // 35
proxy.title // 35

下面是对一个空对象进行拦截,而且实现了一个空拦截。

var fproxy = new Proxy({}, {});
console.log(fproxy.prototype === Object.prototype);  //false

空函数拦截

var fproxy = new Proxy(function(){}, {});
console.log(fproxy.prototype === Object.prototype);  //false

例如 apply(target, object, args) 可以拦截 Proxy 实例作为函数调用的操作,比如 proxy(...args)proxy.call(object, ...args)proxy.apply(...)

var fproxy = new Proxy(function(){}, {
    apply: function(target, object, args){
        return args[0];
    }
})

fproxy(1, 2, 3);  //1

what? 为什么会输出 1 呢? 在 JS 中每个函数都包含两个非继承而来的方法 call 和 apply。 这两个方法的用途都是在特定的作用域中调用函数,实际上等于设置函数体内的this对象的值

apply 方法 : 接收两个参数,第一个参数是在其中运行函数的作用域,第二个是一个参数数组或者 arguments 对象。

call 方法 :与 apply 方法作用相同,第一个参数也相同,区别在于,其余的参数需要逐个列出。

apply(thisArg, argArray);
call(thisArg[,arg1,arg2…]);

来,我们看一个例子:

var name = "LiXiaoQiang";
function sayName(){
    console.log(this.name);
}

console.log(sayName());  //undefined

上面实际上默认执行了函数 sayName 的 apply 方法,该方法将 this 绑定到了 sayName 内部。接下来我们使用 apply 绑定到外部作用域。

var name = "LiXiaoQiang";
function sayName(){
    console.log(this.name);
}

console.log(sayName.apply(this));  //LiXiaoQiang

还可以这么用

var person = {
    name: "ZhangQiang"
}

var name = "LiXiaoQiang";
function sayName(){
    console.log(this.name);
}

console.log(sayName.apply(person));  //ZhangQiang

也就是说 JS 中函数(Function 对象)在被调用的时候会默认调用 apply 和 call 方法将 this 绑定到该对象上,事实上 Proxy 支持拦截的方法很多,其中 apply 只是其中的一个而已。

代理的 this 问题

在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m()  // true

异步编程 Promise

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

Promise实例生成以后,可以用 then 方法分别指定 resolved 状态和 rejected 状态的回调函数。

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

Promise 实例具有 then 方法,也就是说,then 方法是定义在原型对象 Promise.prototype 上的。它的作用是为 Promise 实例添加状态改变时的回调函数。

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

新数据类型 Symbol

ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

Symbol 值通过 Symbol 函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

let s = Symbol();

typeof s  //"symbol"

Symbol 函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

let s1 = Symbol('foo');
let s2 = Symbol('bar');

console.log(s1);  //Symbol(foo)
console.log(s2);  //Symbol(bar)

console.log(s1.toString());  //"Symbol(foo)"
console.log(s2.toString());  //"Symbol(bar)"

Symbol 值不能与其他类型的值进行运算,会报错。

let sym = Symbol('My symbol');
console.log("your symbol is " + sym);  //Error

但是,Symbol 值可以显式转为字符串。

let sym = Symbol('My symbol');

String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)

Symbol 除了使用 toString 显示描述信息外,ES2019 提供了一个实例属性 description,直接返回 Symbol 的描述。

const sym = Symbol('foo');
console.log(sym.description);

由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。

let mySymbol = Symbol();

// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';

// 第二种写法
let a = {
  [mySymbol]: 'Hello!'
};

// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });

// 以上写法都得到同样结果
console.log(a[mySymbol]);

同样的,如果是字面量对象:

let s = Symbol();

let obj = {
  [s]: function (arg) {
     console.log(arg);
  }
};

console.log(obj[s](123));

上面代码中,如果 s 不放在方括号中,该属性的键名就是字符串 s,而不是 s 所代表的那个 Symbol 值。

采用增强的对象写法,上面代码的 obj 对象可以写得更简洁一些。

let obj = {
  [s](arg) { ... }
};

Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。

const log = {};

log.levels = {
  DEBUG: Symbol('debug'),
  INFO: Symbol('info'),
  WARN: Symbol('warn')
};
console.log(log.levels.DEBUG, 'debug message');
console.log(log.levels.INFO, 'info message');

下面是另外一个例子。

const COLOR_RED    = Symbol();
const COLOR_GREEN  = Symbol();

function getComplement(color) {
  switch (color) {
    case COLOR_RED:
      return COLOR_GREEN;
    case COLOR_GREEN:
      return COLOR_RED;
    default:
      throw new Error('Undefined color');
    }
}

常量使用 Symbol 值最大的好处,就是其他任何值都不可能有相同的值了,因此可以保证上面的switch语句会按设计的方式工作。

还有一点需要注意,Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。

有时,我们希望重新使用同一个 Symbol 值,Symbol.for() 方法可以做到这一点。

let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');

console.log(s1 === s2); // true

Symbol.for() 与 Symbol() 这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.keyFor() 方法返回一个已登记的 Symbol 类型值的 key。

let s1 = Symbol.for("foo");
Symbol.keyFor(s1) // "foo"

let s2 = Symbol("foo");
Symbol.keyFor(s2) // undefined

Map 和 Set

ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

const s = new Set();

[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));

for (let i of s) {
  console.log(i);
}

ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

这里的 Set 和 Map 和 Java 中的概念比较类似,还是容易理解的。