Open mowatermelon opened 5 years ago
ES5 只有全局作用域
和函数作用域
,没有块级作用域
,这带来很多不合理的场景。
第一种场景
内层变量可能会覆盖外层变量。
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
上面代码的原意是,if
代码块的外部使用外层的tmp
变量,内部使用内层的tmp
变量。
但是,函数f
执行后,输出结果为undefined
,原因在于变量提升,导致内层的tmp
变量覆盖了外层的tmp
变量。
第二种场景
用来计数的循环变量泄露为全局变量。
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
上面代码中,变量i
只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。
let
实际上为 JavaScript
新增了块级作用域
。
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
上面的函数有两个代码块,都声明了变量n
,运行后输出 5
。这表示外层代码块
不受内层代码块
的影响。
如果两次都使用var
定义变量n
,最后输出的值才是 10
。
ES6 允许块级作用域的任意嵌套
上面代码使用了一个五层的块级作用域。
{{{{{let insane = 'Hello World'}}}}};
外层作用域
无法读取内层作用域
的变量。
{{{{
{let insane = 'Hello World'}
console.log(insane); // 报错
}}}};
内层作用域可以定义外层作用域的同名变量
{{{{
let insane = 'Hello World';
{let insane = 'Hello World'}
}}}};
块级作用域
的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)
不再必要了。
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
ES5中函数声明
函数
能不能在块级作用域
之中声明?这是一个相当令人混淆的问题。
ES5
规定,函数只能在顶层作用域
和函数作用域
之中声明,不能在块级作用域
声明。
// 情况一
if (true) {
function f() {}
}
// 情况二
try {
function f() {}
} catch(e) {
// ...
}
上面两种函数声明,根据 ES5
的规定都是非法的。
但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域
之中声明函数
,因此上面两种情况实际都能运行,不会报错。
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
上面代码在 ES5
中运行,会得到I am inside!
,因为在if
内声明的函数f
会被提升到函数头部
,实际运行的代码如下。
// ES5 环境
function f() { console.log('I am outside!'); }
(function () {
function f() { console.log('I am inside!'); }
if (false) {
}
f();
}());
ES6中函数声明
ES6
引入了块级作用域
,明确允许在块级作用域之中声明函数。ES6
规定,块级作用域
之中,函数声明
语句的行为类似于let
,在块级作用域
之外不可引用。
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
if (false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
符合 ES6
的浏览器中运行,理论上会得到I am outside!
。因为块级作用域
内声明的函数类似于let
,对作用域之外没有影响。
但是,如果你真的在 ES6
浏览器中运行一下上面的代码,是会报错的,这是为什么呢?
因为实际运行的是下面的代码。
// 浏览器的 ES6 环境
function f() { console.log('I am outside!'); }
(function () {
var f = undefined;
if (false) {
function f() { console.log('I am inside!'); }
}
f();
}());
// Uncaught TypeError: f is not a function
原来,如果改变了块级作用域
内声明的函数的处理规则
,显然会对老代码
产生很大影响。
为了减轻因此产生的不兼容问题,ES6
在附录 B
里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式
。
ES6中在块级作用域内声明函数规则
函数声明
类似于var
,即会提升到全局作用域
或函数作用域
的头部。函数声明
还会提升到所在的块级作用域
的头部
。注意,上面三条规则只对 ES6
的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域
的函数声明
当作let
处理。
根据这三条规则,在浏览器的 ES6
环境中,块级作用域
内声明的函数,行为类似于var
声明的变量。
考虑到环境导致的行为差异太大,应该避免在块级作用域
内声明函数
。如果确实需要,也应该写成函数表达式
,而不是函数声明语句
。
块级作用域中函数声明需要使用大括号
ES6
的块级作用域
允许声明函数
的规则,只在使用大括号
的情况下成立,如果没有使用大括号
,就会报错。
// 不报错
'use strict';
if (true) {
function f() {}
}
// 报错
'use strict';
if (true)
function f() {}
我们知道ES6
之前没有块级作用域
,只有全局作用域
和函数作用域
。
JS
在执行脚本之前会先解析代码
,在解析
的时候会创建一个全局执行上下文,并将其中的变量
、函数
都先拿出来,并给它们提前在内存
中开辟好空间,变量暂时赋值为undefined
,函数
则会提前声明,整个存储在内存中,这一步做完了再正式执行
程序。
函数在执行
的时候同理,也会先解析
代码,创建一个函数执行上下文
,将其中的变量
、函数
提前准备好。
console.log(a); // undefined
var a = 1;
test(); // test is running
function test(){
console.log('test is running')
}
b=2;
所以,当执行console.log(a)
的时候,JS解析器
已经提前把a
定义好并赋值为undefined
。可以在函数定义前就调用。
我们在使用变量
或函数
的时候,理解什么时候被初始化值
的是至关重要。
变量提升
是指在声明
一个变量
之前就使用了变量
,在全局作用域
中,只有使用var
关键字声明
的变量
才会变量提升
,变量提升
的时候浏览器
只知道有这么一个变量
。
但你下面定义的值还没有赋值
给这个变量
,这时候·的值是undefined
的,等到浏览器执行到下面的代码的时候才是一个赋值
的过程。
所以变量提升
的时候没有初始化值。用var
声明变量
的时候会给window
增加一个相同变量名
的属性
,所以你也可以通过属性名
的方式获取这个变量
的值,当没有使用任何关键字声明
时,只是给一个变量
赋值时,变量
也相当于给window
增加一个相同变量名
的属性
。
定义一个函数可以使用函数声明
和函数表达式
,这两种方式在提升的时候也是有区别的,函数声明
会提升到作用域
的顶部
,在提升
的时候会分配一个内存空间
,变量
指向这个函数的内存空间
。
所以在定义一个函数
之前是可以执行这个函数
的,函数声明
的方式定义函数会提升。而函数表达式
就跟变量提升
,仅仅只是声明
,并没有给其赋值
。
// 函数声明语句
{
let a = 'secret';
function f() {
return a;
}
}
// 函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
暂时性死区
(temporal dead zone,简称 TDZ),ES6
明确规定,如果区块中存在let
和const
命令,这个区块
对这些命令声明的变量
,从一开始就形成了封闭作用域
。凡是在声明
之前就使用
这些变量,就会报错。
暂时性死区
的本质就是,只要一进入当前作用域
,所要使用的变量
就已经存在了,但是不可获取
,只有等到声明变
量的那一行代码出现,才可以获取
和使用
该变量
。
ES6
规定暂时性死区
和let
、const
语句不出现变量提升
,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。
这样的错误在 ES5
是很常见的,现在有了这种规定,避免此类错误就很容易了。
ES5
只有两种声明变量的方法:var
命令和function
命令。
ES6
除了添加let
和const
命令,后面章节还会提到,另外两种声明变量的方法:import
命令和class
命令。
所以,ES6
一共有 6 种声明变量的方法。
ES5
的顶层对象
,本身也是一个问题,因为它在各种实现里面是不统一的。
浏览器里面,顶层对象是window
,但 Node
和 Web Worker
没有window
。
浏览器和 Web Worker
里面,self
也指向顶层对象,但是 Node
没有self
。
Node
里面,顶层对象是global
,但其他环境都不支持。
同一段代码为了能够在各种环境
,都能取到顶层对象
,现在一般是使用this
变量,但是有局限性。
全局环境
中,this
会返回顶层对象
。但是,Node
模块和 ES6
模块中,this
返回的是当前模块
。
函数里面的this
,如果函数不是作为对象
的方法运行
,而是单纯作为函数
运行,this
会指向顶层对象
。但是,严格模式
下,这时this
会返回undefined
。
不管是严格模式
,还是普通模式
,new Function('return this')()
,总是会返回全局对象
。
但是,如果浏览器用了 CSP
(Content Security Policy
,内容安全策略
),那么eval
、new Function
这些方法都可能无法使用。
综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象
。下面是两种勉强可以使用的方法。
// 方法一
(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');
};
现在有一个提案
,在语言标准的层面,引入global
作为顶层对象
。也就是说,在所有环境下,global
都是存在的,都可以从它拿到顶层对象
。
垫片库system.global
模拟了这个提案
,可以在所有环境拿到global
。
// CommonJS 的写法
require('system.global/shim')();
// ES6 模块的写法
import shim from 'system.global/shim'; shim();
上面代码可以保证各种环境里面,global
对象都是存在的。
// CommonJS 的写法
var global = require('system.global')();
// ES6 模块的写法
import getGlobal from 'system.global';
const global = getGlobal();
上面代码将顶层对象放入变量global
。
顶层对象,在浏览器环境指的是window
对象,在 Node
指的是global
对象。ES5
之中,顶层对象
的属性与全局变量
是等价的。
window.a = 1;
a // 1
a = 2;
window.a // 2
上面代码中,顶层对象
的属性
赋值与全局变量
的赋值
,是同一件事。
顶层对象
的属性与全局变量挂
钩,被认为是 JavaScript
语言最大的设计败笔之一。
这样的设计带来了几个很大的问题
首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);
其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。
另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。
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
。
ES6
新增了let
命令,用来声明变量。它的用法类似于var
,但是所声明的变量,只在let
命令所在的代码块内有效。
{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
上面代码
在代码块
之中,分别用let
和var
声明了两个变量
。然后在代码块
之外调用这两个变量,结果let
声明的变量
报错,var
声明的变量返回了正确的值。这表明,let
声明的变量
只在它所在的代码块
有效。
for
循环的计数器,就很合适使用let
命令,可以减少全局变量
的污染
和变量作用域
的错误访问
。
for (let i = 0; i < 10; i++) {
// ...
}
console.log(i);
// ReferenceError: i is not defined
上面代码中,计数器i
只在for
循环体内有效,在循环体
外引用就会报错。
var和let在for循环中使用对比
下面的代码如果使用var
,最后输出的是10
。
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
上面代码中,变量i
是var
命令声明的,在全局范围内都有效,所以全局只有一个变量i
。
每一次循环,变量i
的值都会发生改变,而循环内被赋给数组a
的函数内部的console.log(i)
,里面的i
指向的就是全局的i
。
也就是说,所有数组a
的成员里面的i
,指向的都是同一个i
,导致运行时输出的是最后一轮的i
的值,也就是 10
。
如果使用let
,声明的变量仅在块级作用域
内有效,最后输出的是 6
。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
上面代码中,变量i
是let
声明的,当前的i
只在本轮循环有效,所以每一次循环的i
其实都是一个新的变量,所以最后输出的是6
。你可能会问,如果每一轮循环的变量i
都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript
引擎内部会记住上一轮循环的值,初始化本轮的变量i
时,就在上一轮循环的基础上进行计算。
for循环单独作用域
for循环
还有一个特别之处,就是设置循环变量
的那部分是一个父作用域
,而循环体内部
是一个单独的子作用域
。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
上面代码正确运行,输出了 3
次abc
。这表明函数内部
的变量i
与循环变量i
不在同一个作用域
,有各自单独的作用域
。
var
命令会发生变量提升
现象,即变量可以在声明之前使用,值为undefined
。
这种现象多多少少是有些奇怪的,按照一般的逻辑,变量
应该在声明
语句之后才可以使用
。
为了纠正这种现象,let
命令改变了语法行为,它所声明
的变量一定要在声明
后使用
,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
上面代码中,变量foo
用var
命令声明,会发生变量提升,即脚本开始运行时,变量foo
已经存在了,但是没有值,所以会输出undefined
。
变量bar
用let
命令声明,不会发生变量提升。这表示在声明它之前,变量bar
是不存在的,这时如果用到它,就会抛出一个错误。
基础案例
只要块级作用域内存在let
命令,它所声明的变量就绑定
(binding
)这个区域,不再受外部
的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
上面代码中,存在全局变量tmp
,但是块级作用域内let
又声明了一个局部变量tmp
,导致后者绑定这个块级作用域
,所以在let
声明变量前,对tmp
赋值会报错。
总之,在代码块内,使用let
命令声明变量之前,该变量都是不可用的。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
上面代码中,在let
命令声明变量tmp
之前,都属于变量tmp
的死区
。
ES6中
typeof
的不安全性
暂时性死区
也意味着typeof
不再是一个百分之百安全的操作。
typeof x; // ReferenceError
let x;
上面代码中,变量x
使用let
命令声明,所以在声明之前,都属于x
的死区
,只要用到该变量就会报错。因此,typeof
运行时就会抛出一个ReferenceError
。
作为比较,如果一个变量
根本没有被声明
,使用typeof
反而不会报错。
typeof undeclared_variable // "undefined"
上面代码中,undeclared_variable
是一个不存在的变量名,结果返回undefined
。
所以,在没有let
之前,typeof
运算符是百分之百安全的,永远不会报错。
现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明
之后使用
,否则就报错。
ES6中隐蔽死区
有些死区
比较隐蔽,不太容易发现。
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
// Uncaught ReferenceError: y is not defined
上面代码中,调用bar
函数之所以报错(某些实现可能不报错),是因为参数x
默认值等于另一个参数y
,而此时y
还没有声明,属于死区
。
如果y
的默认值是x
,就不会报错,因为此时x
已经声明了。
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
另外,下面的代码也会报错,与var
的行为不同。
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
上面代码报错,也是因为暂时性死区
。使用let
声明变量时,只要变量在还没有声明完成前使用,就会报错。
上面这行就属于这个情况,在变量x
的声明语句还没有执行完成前,就去取x
的值,导致报错x 未定义
。
let
不允许在相同作用域
内,重复声明同一个变量。
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
因此,不能在函数内部重新声明参数。
function func(arg) {
let arg; // 报错
}
function func(arg) {
{
let arg; // 不报错
}
}
const
声明一个只读
的常量
。
一旦声明,常量
的值就不能改变
。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
上面代码表明改变常量的值会报错。
const
声明的变量不得改变值,这意味着,const
一旦声明变量,就必须立即初始化,不能留到以后赋值。
const
实际上保证的,并不是变量
的值
不得改动
,而是变量
指向的那个内存地址
所保存的数据
不得改动
。
对于简单类型的数据(数值
、字符串
、布尔值
),值
就保存在变量
指向的那个内存地址
,因此等同于常量
。
但对于复合类型的数据(主要是对象
和数组
),变量
指向的内存地址
,保存的只是一个指向实际数据
的指针
,const
只能保证这个指针
是固定
的(即总是指向另一个固定
的地址)
,至于它指向的数据结构
是不是可变
的,就完全不能控制了。
因此,将一个对象声明为常量
必须非常小心。
声明对象常量
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
上面代码中,常量foo
储存的是一个地址
,这个地址
指向一个对象
。
不可变
的只是这个地址
,即不能把foo
指向另一个地址
,但对象
本身是可变
的,所以依然可以为其添加新属性
。
声明数组常量
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错
上面代码中,常量a
是一个数组,这个数组
本身是可写的,但是如果将另一个数组
赋值给a
,就会报错。
冻结对象案例
如果真的想将对象冻结
,应该使用Object.freeze
方法。
const foo = Object.freeze({});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;
上面代码中,常量foo
指向一个冻结
的对象
,所以添加新属性不起作用,严格模式
时还会报错。
除了将对象
本身冻结
,对象
的属性
也应该冻结
。
下面是一个将对象彻底冻结的函数。
var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};
const foo;
// SyntaxError: Missing initializer in const declaration
上面代码表示,对于const
来说,只声明不赋值,就会报错。
const
的作用域与let
命令相同:只在声明所在的块级作用域
内有效。
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined
const
命令声明的常量也是不提升
,同样存在暂时性死区
,只能在声明
的位置后面使用。
if (true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}
上面代码在常量MAX
声明之前就调用,结果报错。
const
声明的常量,也与let
一样不可重复声明
。
var message = "Hello!";
let age = 25;
// 以下两行都会报错
const message = "Goodbye!";
const age = 30;
以前,为变量
赋值,只能直接指定值,ES6
允许按照一定模式
,从数组
和对象
中提取值,对变量进行赋值
,这被成为解构(Desructuring
)。
本质上,这种写法属于模式匹配
,只要等号两边的模式相同,左边的变量就会被赋予对应的值。
完全解构
(function (log) {
// 基础数组解构
const [a, b, c] = [1, 2, 3];
log(a); // 1
log(b); // 2
log(c); // 3
// 嵌套数组解构
const [foo, [[bar], baz]] = [1, [[2], 3]];
log(foo); // 1
log(bar); // 2
log(baz); // 3
const [ , , third] = ["foo", "bar", "baz"];
log(third); // "baz"
const [x, , y] = [1, 2, 3];
log(x); // 1
log(y); // 3
const [head, ...tail] = [1, 2, 3, 4];
log(head); // 1
log(tail); // [2, 3, 4]
const [e, f, ...g] = ['a'];
log(e); // "a"
log(f); // undefined
log(g); // []
})(console.log)
不完全解构
另一种情况是不完全解构
,即等号左边的模式,只匹配一部分
的等号右边的数组。这种情况下,解构依然可以成功。
(function (log) {
const [x, y] = [1, 2, 3];
log(x); // 1
log(y); // 2
const [a, [b], d] = [1, [2, 3], 4];
log(a); // 1
log(b); // 2
log(d); // 4
})(console.log)
上面两个例子,都属于不完全解构
,但是可以成功。
Set结构解构
对于 Set
结构,也可以使用数组
的解构赋值。
(function (log) {
const [x, y, z] = new Set(['a', 'b', 'c']);
log(x); // "a"
})(console.log)
Generator 函数解构
事实上,只要某种数据结构具有 Iterator
接口,都可以采用数组
形式的解构赋值
。
(function (log) {
function* fibs() {
const a = 0;
const b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const [first, second, third, fourth, fifth, sixth,seventh] = fibs();
// yield 等待的是未做值替换之前的a值
// 第一次 a 为 0 ,b 为 1
// 第二次 a 为 1 ,b 为 1
// 第三次 a 为 1 ,b 为 2
// 第四次 a 为 2 ,b 为 3
// 第五次 a 为 3 ,b 为 5
// 第六次 a 为 5 ,b 为 8
log(first); // 0
log(second); // 1
log(third); // 1
log(fourth); // 2
log(fifth); // 3
log(sixth); // 5
log(seventh); // 8
})(console.log)
上面代码中,fibs
是一个 Generator
函数(参见《Generator 函数》
一章),原生具有 Iterator
接口。解构赋值会依次从这个接口
获取值。
基础使用
解构赋值允许指定默认值。
(function (log) {
const [foo = true] = [];
log(foo); // true
const [x, y = 'b'] = ['a'];
log(x);// 'a'
log(y);// 'b'
const [a, b = 'b'] = ['a', undefined];
log(a);// 'a'
log(b);// 'b'
})(console.log)
使用表达式
如果默认值是一个表达式
,那么这个表达式
是惰性求值
的,即只有在用到的时候,才会求值
。
(function (log) {
function f() {
console.log('aaa');
}
const [x = f()] = [1];
log(x);// 1
})(console.log)
上面代码中,因为x
能取到值,所以函数f
根本不会执行。上面的代码其实等价于下面的代码。
(function (log) {
const x;
if ([1][0] === undefined) {
x = f();
} else {
x = [1][0];
}
log(x);// 1
})(console.log)
引用其他变量值
默认值
可以引用解构赋值
的其他变量
,但该变量
必须已经声明
。
(function (log) {
const [x1 = 1, y1 = x1] = [];
log(x1);// 1
log(y1);// 1
const [x2 = 1, y2 = x2] = [2];
log(x2);// 2
log(y2);// 3
const [x3 = 1, y3 = x3] = [1, 2];
log(x3);// 1
log(y3);// 2
const [x4 = y4, y4 = 1] = []; // ReferenceError: y is not defined
})(console.log)
上面最后一个表达式
之所以会报错,是因为x4
用y4
做默认值时,y4
还没有声明
。
解构不成功
如果解构不成功,变量的值就等于undefined
。
const [foo] = [];
const [bar, foo] = [1];
以上两种情况都属于解构不成功,foo
的值都会等于undefined
。
不可遍历结构
如果等号的右边不是数组
(或者严格地说,不是可遍历
的结构,参见《Iterator》
一章),那么将会报错。
(function (log) {
// 报错
const [foo1] = 1;// TypeError: 1 is not iterable
const [foo2] = false;// TypeError: false is not iterable
const [foo3] = NaN;// TypeError: NaN is not iterable
const [foo4] = undefined;// TypeError: undefined is not iterable
const [foo5] = null;// TypeError: null is not iterable
const [foo6] = {};// TypeError: {} is not iterable
})(console.log)
上面的语句都会报错
,因为等号右边的值,要么转为对象
以后不具备 Iterator
接口(前五个表达式),要么本身
就不具备 Iterator
接口(最后一个表达式)。
基础使用
解构
不仅可以用于数组
,还可以用于对象
。
对象
的解构
与数组
有一个重要的不同。数组的元素是按次序
排列的,变量
的取值
由它的位置
决定;
而对象
的属性
没有次序
,变量
必须与属性
同名,才能取到正确
的值。
(function (log) {
const {
bar,
foo
} = {
foo: "aaa",
bar: "bbb"
};
log(foo); // "aaa"
log(bar); // "bbb"
const {
baz
} = {
foo: "aaa",
bar: "bbb"
};
log(baz); // undefined
})(console.log)
上面代码的第一个例子,等号左边的两个变量的次序
,与等号右边两个同名属性的次序
不一致,但是对取值完全没有影响。
第二个例子的变量
没有对应的同名属性
,导致取不到值,最后等于undefined
。
变量名与属性名不一致
如果变量名
与属性名
不一致,必须写成下面这样。
(function (log) {
const {
foo: baz
} = {
foo: 'aaa',
bar: 'bbb'
};
log(baz); // "aaa"
const obj = {
first: 'hello',
last: 'world'
};
const {
first: f,
last: l
} = obj;
log(f); // 'hello'
log(l); // 'world'
})(console.log)
这实际上说明,对象
的解构赋值
是下面形式的简写
(参见《对象的扩展》
一章)。
(function (log) {
const {
foo: foo,
bar: bar
} = {
foo: "aaa",
bar: "bbb"
};
log(foo); // "aaa"
log(bar); // "bbb"
})(console.log)
也就是说,对象的解构赋值
的内部机制
,是先找到同名属性
,然后再赋给对应的变量
。
真正被赋值
的是后者
,而不是前者
。
(function (log) {
const {
foo: baz
} = {
foo: "aaa",
bar: "bbb"
};
log(baz); // "aaa"
log(foo); // ReferenceError: foo is not defined
})(console.log)
上面代码中,foo
是匹配的模式
,baz
才是变量
。
真正被赋值
的是变量baz
,而不是模式foo
。
解构对象方法
对象
的解构赋值
,可以很方便地将现有对象
的方法,赋值到某个变量
。
(function (log) {
const {
log: log1,
sin,
cos
} = Math;
log(log1); // [Function: log]
log(sin); // [Function: sin]
log(cos); // [Function: cos]
})(console.log)
上面代码将Math
对象的对数
、正弦
、余弦
三个方法,赋值
到对应的变量
上,使用起来就会方便很多。
属性名表达式
由于数组
本质是特殊的对象
,因此可以对数组
进行对象属性
的解构
。
(function (log) {
const arr = [1, 2, 3];
const {
0: first,
[arr.length - 1]: last
} = arr;
log(first); // 1
log(last); // 3
})(console.log)
上面代码对数组进行对象解构。数组arr
的0
键对应的值是1
,[arr.length - 1]
就是2
键,对应的值是3
。
方括号
这种写法,属于属性名表达式
(参见《对象的扩展》
一章)。
基础嵌套对象解构
与数组
一样,解构也可以用于嵌套结构
的对象
。
(function (log) {
const obj = {
p: [
'Hello',
{
y: 'Melon'
}
]
};
const {
p: [x, {
y
}]
} = obj;
log(x); // "Hello"
log(y); // "Melon"
})(console.log)
注意,这时p
是模式
,不是变量
,因此不会被赋值
。
如果p
也要作为变量
赋值,可以写成下面这样。
(function (log) {
const obj = {
p: [
'Hello',
{
y: 'Melon'
}
]
};
const {
p,
p: [x, {
y
}]
} = obj;
log(x); // "Hello"
log(y); // "Melon"
log(p); // [ 'Hello', { y: 'Melon' } ]
})(console.log)
多重嵌套对象解构
与数组
一样,解构也可以用于嵌套结构
的对象
。
(function (log) {
const node = {
loc: {
start: {
line: 1,
column: 5
}
}
};
const {
loc,
loc: {
start
},
loc: {
start: {
line
}
}
} = node;
log(line); // 1
log(loc); // { start: { line: 1, column: 5 } }
log(start); // { line: 1, column: 5 }
})(console.log)
上面代码有三次解构赋值,分别是对loc
、start
、line
三个属性
的解构赋值
。
注意,最后一次对line
属性的解构赋值
之中,只有line
是变量
,loc
和start
都是模式
,不是变量
。
结合数组进行嵌套赋值
(function (log) {
const obj = {};
const arr = [];
({
foo: obj.prop,
bar: arr[0]
} = {
foo: 123,
bar: true
});
log(obj); // { prop: 123 }
log(arr); // [ true ]
})(console.log)
对象
的解构
也可以指定默认值
。
(function (log) {
const {
x1 = 3
} = {};
log(x1); // 3
const {
x2,
y2 = 5
} = {
x2: 1
};
log(x2); // 1
log(y2); // 5
const {
x3: y3 = 3
} = {};
// log(x3); // ReferenceError: x3 is not defined
log(y3); // 3
const {
x4: y4 = 3
} = {
x: 5
};
// log(x4); // ReferenceError: x4 is not defined
log(y4); // 3
const {
message: msg = 'Something went wrong'
} = {};
log(msg); // "Something went wrong"
})(console.log)
默认值
生效的条件是,对象的属性值
严格等于undefined
。
(function (log) {
const {
x1 = 3
} = {
x1: undefined
};
log(x1); // 3
const {
x2 = 3
} = {
x2: null
};
log(x2); // null
})(console.log)
上面代码中,属性x
等于null
,因为null
与undefined
不严格相等,所以是个有效的赋值
,导致默认值3
不会生效。
解构不成功
如果解构不成功,变量的值就等于undefined
。
(function (log) {
const {foo} = {bar: 'baz'};
log(foo); // undefined
})(console.log)
父属性不存在
如果解构模式是嵌套
的对象,而且子对象
所在的父属性
不存在,那么将会报错
。
(function (log) {
// 报错
const {foo: {bar}} = {baz: 'baz'};// TypeError: Cannot destructure property `bar` of 'undefined' or 'null'.
const _tmp = {baz: 'baz'};
_tmp.foo.bar // Cannot read property 'bar' of undefined
})(console.log)
上面代码中,等号左边对象的foo
属性,对应一个子对象
。该子对象
的bar
属性,解构
时会报错。
原因很简单,因为foo
这时等于undefined
,类似于上文的_tmp.foo.bar
,再取子属性
就会报错。
已经声明的变量
如果要将一个已经声明
的变量
用于解构赋值
,必须非常小心。
(function (log) {
// 错误的写法
const x;
{x} = {x: 1};
// SyntaxError: Unexpected token =
})(console.log)
上面代码的写法会报错,因为 JavaScript
引擎会将{x}
理解成一个代码块,从而发生语法错误
。
只有不将大括号
写在行首
,避免 JavaScript
将其解释为代码块
,才能解决这个问题。
(function (log) {
// 正确的写法
const x;
({x} = {x: 1});
log(x);// 1
})(console.log)
上面代码将整个解构赋值
语句,放在一个圆括号
里面,就可以正确执行。
基础使用
函数
的参数
也可以使用解构赋值
。
(function (log) {
function add([x, y]) {
return x + y;
}
log(add([1, 2])); // 3
})(console.log)
上面代码中,函数add
的参数
表面上是一个数组
,但在传入参数
的那一刻,数组参数
就被解构成变量x
和y
。对于函数内部
的代码来说,它们能感受到的参数就是x
和y
。
结合箭头函数使用
函数
的参数
也可以使用解构赋值
。
(function (log) {
const arr = [
[1, 2],
[3, 4]
].map(([a, b]) => a + b);
log(arr); // [ 3, 7 ]
})(console.log)
函数参数
的解构
也可以使用默认值
。
函数对象参数属性使用默认值
(function (log) {
function move({
x = 0,
y = 0
} = {}) {
return [x, y];
}
log(move({
x: 3,
y: 8
})); // [3, 8]
log(move({
x: 3
})); // [3, 0]
log(move({})); // [0, 0]
log(move()); // [0, 0]
})(console.log)
上面代码中,函数move
的参数
是一个对象
,通过对这个对象
进行解构
,得到变量x
和y
的值。如果解构
失败,x
和y
等于默认值
。
函数对象参数使用默认值
(function (log) {
function move({
x,
y
} = {
x: 0,
y: 0
}) {
return [x, y];
}
log(move({
x: 3,
y: 8
})); // [3, 8]
log(move({
x: 3
})); // [3, undefined]
log(move({})); // [undefined, undefined]
log(move()); // [0, 0]
})(console.log)
上面代码是为函数move
的参数
指定默认值
,而不是为变量x
和y
指定默认值
,所以会得到与前一种写法不同的结果。
undefined
就会触发函数参数
的默认值
。
(function (log) {
const arr = [1, undefined, 3].map((x = 'yes') => x);
log(arr); // [ 1, 'yes', 3 ]
})(console.log)
字符串
也可以解构赋值
。这是因为此时,字符串
被转换成了一个类似数组
的对象。
(function (log) {
const [a, b, c, d, e] = 'hello';
log(a); // "h"
log(b); // "e"
log(c); // "l"
log(d); // "l"
log(e); // "o"
})(console.log)
类似数组
的对象
都有一个length
属性,因此还可以对这个属性
解构赋值。
(function (log) {
const {
length: len
} = 'hello';
log(len); // 5
})(console.log)
解构赋值
时,如果等号右边是数值
和布尔值
,则会先转为对象
。
(function (log) {
const {
toString: s1
} = 123;
log(s1 === Number.prototype.toString); // true
const {
toString: s2
} = true;
log(s2 === Boolean.prototype.toString); // true
})(console.log)
上面代码中,数值
和布尔值
的包装对象
都有toString
属性,因此变量s1
和变量s2
都能取到值
。
解构赋值
虽然很方便,但是解析
起来并不容易。
对于编译器
来说,一个式子
到底是模式
,还是表达式
,没有办法从一开始就知道,必须解析
到(或解析不到)等号
才能知道。
由此带来的问题是,如果模式中出现圆括号
怎么处理。ES6
的规则是,只要有可能导致解构
的歧义
,就不得使用圆括号
。
但是,这条规则实际上不那么容易辨别
,处理起来相当麻烦。因此,建议只要有可能,就不要在模式
中放置圆括号
。
不能使用圆括号的情况
以下三种解构赋值不得使用圆括号。
(function (log) {
// 全部报错
const [(a)] = [1];// SyntaxError: Unexpected token (
const {x: (c)} = {};// SyntaxError: Unexpected token (
const ({x: c}) = {};// SyntaxError: Unexpected token (
const {(x: c)} = {};// SyntaxError: Unexpected token (
const {(x): c} = {};// SyntaxError: Unexpected token (
const { o: ({ p: p }) } = { o: { p: 2 } };// SyntaxError: Unexpected token (
})(console.log)
上面 6 个语句都会报错,因为它们都是变量
声明语句,模式
不能使用圆括号
。
函数参数
也属于变量声明
,因此不能带有圆括号
。
// 报错
function f([(z)]) { return z; }// SyntaxError: Unexpected token (
// 报错
function f([z,(x)]) { return x; }// SyntaxError: Unexpected token (
(function (log) {
// 全部报错
({ p: a }) = { p: 42 };// SyntaxError: Unexpected token (
([a]) = [5];// SyntaxError: Unexpected token (
})(console.log)
上面代码将整个模式
放在圆括号
之中,导致报错。
(function (log) {
// 报错
[({ p: a }), { x: c }] = [{}, {}];// SyntaxError: Unexpected token (
})(console.log)
上面代码将一部分模式
放在圆括号
之中,导致报错。
可以使用圆括号
的情况只有一种:赋值语句
的非模式
部分,可以使用圆括号
。
(function (log) {
[(b)] = [3]; // 正确
log(b);// 3
({ p: (d) } = {}); // 正确
log(d);// undefined
[(parseInt.prop)] = [3]; // 正确
log(parseInt.prop);// 3
})(console.log)
上面三行语句都可以正确执行,因为首先它们都是赋值
语句,而不是声明
语句;
其次它们的圆括号
都不属于模式
的一部分。
第一行语句
中,模式
是取数组
的第一个成员,跟圆括号
无关;
第二行语句
中,模式是p
,而不是d
;
第三行语句
与第一行语句
的性质
一致。
解构赋值
允许等号左边的模式
之中,不放置任何变量名
。因此,可以写出非常古怪
的赋值表达式
。
({} = [true, false]);
({} = 'abc');
({} = []);
上面的表达式
虽然毫无意义
,但是语法是合法
的,可以执行。
(function (log) {
let x = 1;
let y = 2;
log(x);// 1
log(y);// 2
[x, y] = [y, x];
log(x);// 2
log(y);// 1
})(console.log)
上面代码交换变量x
和y
的值,这样的写法不仅简洁
,而且易读
,语义
非常清晰。
函数
只能返回一个值
,如果要返回多个值
,只能将它们放在数组
或者对象
里返回。
有了解构赋值
,取出这些值
就非常方便。
(function (log) {
// 返回一个数组
function example1() {
return [1, 2, 3];
}
let [a, b, c] = example1();
log(a); // 1
log(b); // 2
log(c); // 3
// 返回一个对象
function example2() {
return {
foo: 1,
bar: 2
};
}
let {
foo,
bar
} = example2();
log(foo); // 1
log(bar); // 2
})(console.log)
解构赋值
可以方便地将一组参数
与变量名
对应起来。
// 参数是一组有次序的值
function f([x, y, z]) { ... }
f([1, 2, 3]);
// 参数是一组无次序的值
function f({x, y, z}) { ... }
f({z: 3, y: 2, x: 1});
指定参数的默认值
,就避免了在函数体内部再写var foo = config.foo || 'default foo';
这样的语句。
jQuery.ajax = function (url, {
async = true,
beforeSend = function () {},
cache = true,
complete = function () {},
crossDomain = false,
global = true,
// ... more config
} = {}) {
// ... do stuff
};
解构赋值
对提取 JSON
对象中的数据
,尤其有用。
(function (log) {
const jsonData = {
id: 42,
status: "OK",
data: [867, 5309]
};
const {
id,
status,
data: number
} = jsonData;
log(id, status, number);
// 42, "OK", [867, 5309]
})(console.log)
上面代码可以快速
提取 JSON
数据的值。
任何部署了 Iterator 接口
的对象,都可以用for...of
循环遍历。Map
结构原生支持 Iterator 接口
,配合变量
的解构赋值
,获取键名
和键值
就非常方便。
(function (log) {
const map = new Map();
map.set('first', 'hello');
map.set('second', 'melon');
for (let [key, value] of map) {
log(key + " is " + value);
}
// first is hello
// second is melon
})(console.log)
如果只想获取键名
,或者只想获取键值
,可以写成下面这样。
// 获取键名
for (let [key] of map) {
// ...
}
// 获取键值
for (let [,value] of map) {
// ...
}
加载模块
时,往往需要指定需要获取哪些方法
,解构赋值
使得获取语句
非常清晰。
const {a,b} = require('xxx');
const { SourceMapConsumer, SourceNode } = require("source-map");
JavaScript
允许采用\uxxxx
形式表示一个字符,其中xxxx
表示字符的 Unicode 码点
。
但是,这种表示法只限于\u0000-\uFFFF
之间的字符,超过这个范围的字符,必须用两个双字节
的形式表达。
如果直接在\u
后面跟上超过0xFFFF
的数值(比如\u20BB7
),Javascript
会理解为\u20BB+7
,由于\u20BB
是一个不可打印字符,所以会打印一个空格,后面跟着一个7
。
ES6
对于这一点做了改进,只要将超过0xFFFF
的编号放入大括号
,就能得到正确解读。
(function (log) {
log('\u0061');// 'a'
log('\uD842\uDFB7');// 𠮷
log('\u20BB7'); // ₻7
log('\u{20BB7}'); // 𠮷
log('\u{41}\u{42}\u{43}'); // ABC
let hello = 123;
log(hell\u{6F}); // 123
log('\u{1F680}' === '\uD83D\uDE80');
})(console.log)
上面代码中,最后一个例子
表明,大括号表示法
与四字节
的 UTF-16
编码是等价的。
有了这种表示法之后,JavaScript
共有 6
种方法可以表示一个字符
。
(function (log) {
log('\z' === 'z'); // true
log('\172' === 'z'); // true
log('\x7A' === 'z'); // true
log('\u007A' === 'z'); // true
log('\u{7A}' === 'z'); // true
})(console.log)
ES5
提供String.fromCharCode
方法,用于从码点
返回对应字符
,但是这个方法不能识别 32
位的 UTF-16
字符(Unicode
编号大于0xFFFF
)。
(function (log, S) {
log(S.fromCharCode(0x20BB7)); // ஷ
})(console.log, String)
上面代码中,String.fromCharCode
不能识别大于0xFFFF
的码点,所以0x20BB7
就发生了溢出
,最高位2
被舍弃了,最后返回码点U+0BB7
对应的字符
,而不是码点U+20BB7
对应的字符
。
ES6
提供了String.fromCodePoint
方法,可以识别大于0xFFFF
的字符,弥补了String.fromCharCode
方法的不足。在作用上,正好与codePointAt
方法相反。
(function (log, S) {
log(S.fromCodePoint(0x20BB7)); // 𠮷
log(S.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'); // true
})(console.log, String)
上面代码中,如果String.fromCodePoint
方法有多个参数
,则它们会被合并
成一个字符串
返回。
注意,fromCodePoint
方法定义在String
对象上,而codePointAt
方法定义在字符串的实例对象
上。
ES6
还为原生的 String
对象,提供了一个raw
方法。
String.raw
方法,往往用来充当模板字符串
的处理函数
,返回一个斜杠
都被转义
(即斜杠
前面再加一个斜杠
)的字符串
,对应于替换变量
后的模板字符串
。
(function (log, S) {
log(S.raw`Hi\n${2+3}!`);// 'Hi\\n5!'
log(S.raw`Hi\u000A!`);// 'Hi\\u000A!'
})(console.log, String)
如果原字符串
的斜杠
已经转义,那么String.raw
会进行再次转义。
(function (log, S) {
log(S.raw`Hi\\n`);// 'Hi\\n'
})(console.log, String)
String.raw
方法可以作为处理模板字符串
的基本方法,它会将所有变量替换
,而且对斜杠
进行转义
,方便下一步作为字符串
来使用。
String.raw
方法也可以作为正常
的函数
使用。
这时,它的第一个参数,应该是一个具有raw属性
的对象
,且raw属性
的值应该是一个数组
。
(function (log, S) {
log(S.raw({
raw: 'test'
}, 0, 1, 2));
// 't0e1s2t'
// 等同于
log(S.raw({
raw: ['t', 'e', 's', 't']
}, 0, 1, 2));// 't0e1s2t'
})(console.log, String)
作为函数,String.raw
的代码实现基本如下。
String.raw = function (strings, ...values) {
let output = '';
let index;
for (index = 0; index < values.length; index++) {
output += strings.raw[index] + values[index];
}
output += strings.raw[index]
return output;
}
在 Javascript
内部,字符以UTF-16
的格式存储,每个字符固定为2
个字节。
对于那些需要4
个字节存储的字符(Unicode
编号大于0xFFFF
的字符),Javascript
会认为它们是两
个字符。
(function (log) {
const s = '𠮷a';
log(s.length) // 3
log(s.charAt(0)) // �
log(s.charAt(1)) // �
log(s.charCodeAt(0)) // 55362
log(s.charCodeAt(1)) // 57271
})(console.log)
上面代码中,汉字𠮷
(注意,这个字不是吉祥
的吉
)的码点
是0x20BB7
,UTF-16
编码为0xD842 0xDFB7
(十进制为55362 57271
),需要4
个字节储存。
对于这种4
个字节的字符,JavaScript
不能正确处理,字符串长度会误判为2
,而且charAt
方法无法读取整个字符
,charCodeAt
方法只能分别返回前两个字节
和后两个字节
的值
。
ES6
提供了codePointAt
方法,能够正确处理4
个字节存储的字符,返回一个字符的Unicode
码点。
(function (log) {
const s = '𠮷a';
log(s.codePointAt(0)) // 134071
log(s.codePointAt(1)) // 57271
log(s.codePointAt(2)) // 97
})(console.log)
codePointAt
方法的参数,是字符
在字符串
中的位置(从0
开始)。
JavaScript
将𠮷a
视为三个字符,codePointAt
方法在第一个字符上,正确地识别了𠮷
,返回了它的十进制码点 134071
(即十六进制的20BB7
)。
在第二个字符
(即𠮷
的后两个字节
)和第三个字符a
上,codePointAt
方法的结果与charCodeAt
方法相同。
总之,codePointAt
方法可以正确返回四字节UTF-16
字符的Unicode
码点。
对于那些两个字节
存储的常规字符
,它返回结果与charCodeAt
方法相同。
你可能注意到了,codePointAt
方法的参数,仍然是不正确的。
比如,下面代码中,字符a
在字符串s
的正确位置序号应该是 1
,但是必须向codePointAt
方法传入 2
。
解决这个问题的一个办法是使用for...of
循环,因为它会正确识别 32
位的 UTF-16
字符。
(function (log) {
const s = '𠮷a';
log(s.codePointAt(0)) // 134071
log(s.codePointAt(2)) // 97
for (let ch of s) {
log(ch.codePointAt(0));
}
// 134071
// 97
})(console.log)
codePointAt
方法返回的是码点
的十进制值
,如果想要其他进制
的值,可以使用toString
方法转换一下。
(function (log) {
const s = '𠮷a';
log(s.codePointAt(0)) // 134071
log(s.codePointAt(2)) // 97
log(s.codePointAt(0).toString(2)) // 100000101110110111
log(s.codePointAt(2).toString(2)) // 1100001
log(s.codePointAt(0).toString(8)) // 405667
log(s.codePointAt(2).toString(8)) // 141
log(s.codePointAt(0).toString(10)) // 134071
log(s.codePointAt(2).toString(10)) // 97
log(s.codePointAt(0).toString(16)) // 20bb7
log(s.codePointAt(2).toString(16)) // 61
log(s.codePointAt(0).toString(32)) // 42tn
log(s.codePointAt(2).toString(32)) // 31
})(console.log)
codePointAt
方法是测试
一个字符由两个字节
还是四个字节
组成的最简单方法。
(function (log) {
function is32Bit(c) {
return c.codePointAt(0) > 0xFFFF;
}
log(is32Bit('𠮷')); // true
log(is32Bit('a')); // false
})(console.log)
许多欧洲语言有语调符号
和重音符号
。为了表示它们,Unicode
提供了两种方法。
重音符号
的字符,比如Ǒ
(\u01D1
)。合成符号
(combining character
),即原字符
与重音符号
的合成,两个字符合成一个字符,比如O
(\u004F
)和ˇ
(\u030C
)合成Ǒ
(\u004F\u030C
)。这两种表示方法,在视觉
和语义
上都等价
,但是 JavaScript
不能识别。
(function (log, S) {
log('\u01D1' === '\u004F\u030C'); //false
log('\u01D1'.length); // 1
log('\u004F\u030C'.length); // 2
})(console.log, String)
上面代码表示,JavaScript
将合成字符
视为两个字符
,导致两种表示方法不相等。
ES6
提供字符串实例的normalize()
方法,用来将字符
的不同表示方法
统一为同样的形式
,这称为 Unicode
正规化。
(function (log, S) {
const type1 = '\u01D1'.normalize();
const type2 = '\u004F\u030C'.normalize();
log(type1 === type2); // true
})(console.log, String)
normalize
方法可以接受一个参数
来指定normalize
的方式,参数的四个可选值如下。
形参值 | 形参值解释 |
---|---|
NFC |
默认参数,表示标准等价合成 (Normalization Form Canonical Composition ),返回多个简单字符 的合成字符 。所谓标准等价 指的是视觉 和语义 上的等价。 |
NFD |
表示标准等价分解 (Normalization Form Canonical Decomposition ),即在标准等价 的前提下,返回合成字符 分解的多个简单字符 。 |
NFKC |
表示兼容等价合成 (Normalization Form Compatibility Composition ),返回合成字符 。所谓兼容等价 指的是语义 上存在等价,但视觉 上不等价,比如囍 和喜喜 。(这只是用来举例,normalize 方法不能识别中文 。) |
NFKD |
表示兼容等价分解 (Normalization Form Compatibility Decomposition ),即在兼容等价 的前提下,返回合成字符 分解的多个简单字符 。 |
(function (log, S) {
log('\u004F\u030C'.normalize('NFC').length); // 1
log('\u004F\u030C'.normalize('NFD').length); // 2
})(console.log, String)
上面代码表示,NFC
参数返回字符的合成形式
,NFD
参数返回字符的分解形式。
不过,normalize
方法目前不能识别三个
或三个
以上字符
的合成。这种情况下,还是只能使用正则表达式
,通过 Unicode 编号
区间判断。
传统上,Javascript
中只有indexOf
方法,可用来确定一个字符串
是否包含
在另一个字符串
中。ES6
又提供了三种新方法
。
方法名 | 方法解释 |
---|---|
contains(findStr,findIndex) | 返回布尔值,表示了是否找到参数字符串 |
startsWith(findStr,findIndex) | 返回布尔值,表示参数字符串是否正在源字符串的头部 |
endsWith(findStr,findIndex) | 返回布尔值,表示参数字符串是否正在源字符串的尾部 |
findStr
,表示被检索的参数字符串
findIndex
,表示开始搜索的位置,endsWith
的行为与其他两个方法有所不同,它针对前findIndex
个字符,而其他两个方法
则针对从findIndex
个位置直到字符串结束的字符。
(function (log, S) {
const s1 = 'Hello world!';
log(s1.startsWith('Hello')); // true
log(s1.endsWith('!')); // true
log(s1.includes('o')); // true
// 这三个方法都支持第二个参数,表示开始搜索的位置。
const s2 = 'Hello world!';
log(s2.startsWith('world', 6)); // true
log(s2.endsWith('Hello', 5)); // true
log(s2.includes('Hello', 6)); // false
})(console.log, String)
repeat(repeatIndex)
返回一个新字符串
,表示将原字符串
重复repeatIndex
次
(function (log, S) {
log('x'.repeat(3)); // 'xxx'
log('hello'.repeat(2)); // 'hellohello'
log('na'.repeat(0)); // ''
})(console.log, String)
参数值为小数
如果repeat
的参数是小数
,会被取整
,注意这个取整
对当前值不做任何四舍五入
。
(function (log, S) {
log('na'.repeat(2.3)); // 'nana'
log('na'.repeat(2.5)); // 'nana'
log('na'.repeat(2.9)); // 'nana'
log('na'.repeat(2.3999)); // 'nana'
log('na'.repeat(2.599)); // 'nana'
log('na'.repeat(2.999)); // 'nana'
})(console.log, String)
参数值为0
如果repeat
的参数是+0
,-0
,0
,NaN
,-1~0
之间或者0~1
之间值,会返回一个空字符串
。
0
到-1
之间的小数
,则等同于 0
,这是因为会先进行取整
运算。
0
到-1
之间的小数
,取整
以后等于-0
,repeat
视同为 0
。
(function (log, S) {
log('na'.repeat(+0)); // ''
log('na'.repeat(-0)); // ''
log('na'.repeat(-0)); // ''
log('na'.repeat(NaN)); // ''
log('na'.repeat(+0.2)); // ''
log('na'.repeat(+0.5)); // ''
log('na'.repeat(+0.8)); // ''
log('na'.repeat(-0.2)); // ''
log('na'.repeat(-0.5)); // ''
log('na'.repeat(-0.8)); // ''
})(console.log, String)
参数值为
负数
或者Infinity
如果repeat
的参数是负数
或者Infinity
,会报错。
(function (log, S) {
log('na'.repeat(Infinity)); // RangeError: Invalid count value
log('na'.repeat(-1)); // RangeError: Invalid count value
})(console.log, String)
参数值为
字符串
如果repeat
的参数是字符串
,则会先转换成数字
。
字符串
中没有包含有效
的数字
,则当作0
处理。字符串
中包含有效
的数字
,同时又有特殊字符
,\s
,\S
或者字母
等,则当作0
处理。字符串
中包含有效
的数字
,同时仅有空格
,\t
或者\n
等,当作有效参数
进行使用。字符串
中包含其他进制值
的字符串
,转换成十进制
的数值
之后,当作有效参数
进行使用。字符串
中包含符号
的字符串
,转换为数值
之后,如果为大于等于1
的正数
,当作有效参数
进行使用;如果为-1~1
,则当作0
,进行使用;如果为小于等于-1
的负数
,则会抛出异常RangeError: Invalid count value
。(function (log, S) {
log(1, 'na'.repeat('na')); // 1 ''
log(2, 'na'.repeat('3na')); // 2 ''
log(3, 'na'.repeat('3,')); // 3 ''
log(3, 'na'.repeat('3\s')); // 3 ''
log(3, 'na'.repeat('3\S')); // 3 ''
log(4, 'na'.repeat('3 ')); // 4 'nanana'
log(4, 'na'.repeat('3\t')); // 4 'nanana'
log(4, 'na'.repeat('\n3\n')); // 4 'nanana'
log(5, 'na'.repeat('03')); // 5 'nanana'
log(5, 'na'.repeat('0b1')); // 5 'na'
log(5, 'na'.repeat('0o3')); // 5 'nanana'
log(5, 'na'.repeat('0x3')); // 5 'nanana'
// log('na'.repeat('-3')); // RangeError: Invalid count value
log(6, 'na'.repeat('+3')); // 6 'nanana'
})(console.log, String)
参数值为
日期格式
如果repeat
的参数是日期格式
,则会先转换成数字
。
但是由于转换的值
一般超过了可用的字符串长度
,所以一般会直接报错RangeError: Invalid string length
。
如果是传入日期格式单独转换
的天数
和日期值
,repeat
可以正常使用。
(function (log, S) {
const d = Reflect.construct(Date,[]);
log(d.getTime()); // 1549895917860
log(d.getDate()); // 11
log(d.getDay()); // 1
// log('na'.repeat(d)); // RangeError: Invalid string length
// log('na'.repeat(d.getTime())); // RangeError: Invalid string length
log('na'.repeat(d.getDate())); // nanananananananananana
log('na'.repeat(d.getDay())); // na
})(console.log, String)
参数值为
数组
或者对象
如果repeat
的参数是数组
,Map
,WeakMap
,Set
,WeakSet
或者对象
,则会统一当作0
进行处理,返回空字符串
。
(function (log, S) {
log(1, 'na'.repeat([])); // 1 ''
log(1, 'na'.repeat([1, 2, 3])); // 1 ''
log(1, 'na'.repeat([{
z: 1
}])); // 1 ''
log(2, 'na'.repeat({})); // 2 ''
log(2, 'na'.repeat({
z: 1
})); // 2 ''
log(3, 'na'.repeat(new Map())); // 3 ''
log(4, 'na'.repeat(new WeakMap())); // 4 ''
log(5, 'na'.repeat(new Set())); // 5 ''
log(6, 'na'.repeat(new WeakSet())); // 6 ''
})(console.log, String)
ES2017
引入了字符串补全长度的功能。
如果某个字符串
不够指定长度
,会在头部
或尾部
补全。
方法名 | 方法解释 |
---|---|
padStart(maxFillLength,fillStr) | 返回最后补全的字符串,用于头部补全 |
padEnd(maxFillLength,fillStr) | 返回最后补全的字符串,尾部补全 |
maxFillLength
是字符串补全生效的最大长度
,fillStr
是用来补全
的字符串
。(function (log, S) {
const s = 'm';
const fillStr = 'ab';
log(s.padStart(5, fillStr)); // ababm
log(s.padStart(4, fillStr)); // abam
log(s.padEnd(5, fillStr)); // mabab
log(s.padEnd(4, fillStr)); // maba
})(console.log, String)
如果原字符串
的长度
,等于或大于最大长度
,则字符串
补全不生效,返回原字符串
。
(function (log, S) {
const s = 'mm';
const fillStr = 'ab';
log(s.padStart(2, fillStr)); // 'mm'
log(s.padEnd(2, fillStr)); // 'mm'
})(console.log, String)
如果用来补全的字符串
与原字符串
,两者的长度之和超过了最大长度
,则会截去超出位数
的补全字符串
。
(function (log, S) {
const s = 'mm';
const fillStr = '0123456789';
log(s.padStart(10, fillStr)); // 01234567mm
log(s.padEnd(10, fillStr)); // mm01234567
})(console.log, String)
如果省略第二个参数
,默认使用空格
补全长度。
(function (log, S) {
const s = 'mm';
log(s.padStart(10)); // mm
log(s.padEnd(10)); // mm
})(console.log, String)
数值补全指定位数
padStart()
和padEnd
的常见用途
是为数值补全指定位数
。
下面代码生成 10
位的数值字符串
。
(function (log, S) {
const fillLength = 10;
const fillStr = '0';
log('1'.padStart(fillLength, fillStr)); // '0000000001'
log('12'.padStart(fillLength, fillStr)); // '0000000012'
log('123456'.padStart(fillLength, fillStr)); // '0000123456'
log('1'.padEnd(fillLength, fillStr)); // '1000000000'
log('12'.padEnd(fillLength, fillStr)); // '1200000000'
log('123456'.padEnd(fillLength, fillStr)); // '1234560000'
})(console.log, String)
提示字符串格式
padStart()
和padEnd
的还可以用作日期格式
的字符串格式提示
。
下面代码生成年月日
日期格式的字符串。
(function (log, S) {
log('12'.padStart(10, 'YYYY-MM-DD')); // 'YYYY-MM-12'
log('09-12'.padStart(10, 'YYYY-MM-DD')); // 'YYYY-09-12'
// log('12'.padEnd(10, 'YYYY-MM-DD')); // 12YYYY-MM-
// log('09-12'.padEnd(10, 'YYYY-MM-DD')); // 09-12YYYY-
log('2019'.padEnd(10, '-MM-DD')); // '2019-MM-DD'
})(console.log, String)
如果一个正则表达式
在字符串里面有多个匹配
,现在一般使用g
修饰符或y
修饰符,在循环里面逐一取出。
(function (log, S) {
const regex = /t(e)(st(\d?))/g;
const string = 'test1test2test3';
const matches = [];
let match;
while (match = regex.exec(string)) {
matches.push(match);
}
log(matches);
// [ [ 'test1',
// 'e',
// 'st1',
// '1',
// index: 0,
// input: 'test1test2test3',
// groups: undefined ],
// [ 'test2',
// 'e',
// 'st2',
// '2',
// index: 5,
// input: 'test1test2test3',
// groups: undefined ],
// [ 'test3',
// 'e',
// 'st3',
// '3',
// index: 10,
// input: 'test1test2test3',
// groups: undefined ] ]
})(console.log, String)
上面代码中,while
循环取出每一轮的正则匹配
,一共三轮。
目前有一个提案
,目前还未实现,增加了String.prototype.matchAll
方法,可以一次性取出所有匹配
。
不过,它返回的是一个遍历器(Iterator
),而不是数组
。
(function (log, S) {
// g 修饰符加不加都可以
const regex = /t(e)(st(\d?))/g;
const string = 'test1test2test3';
if (string.matchAll) {
for (const match of string.matchAll(regex)) {
log(match);
}
// ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"]
// ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"]
// ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]
} else {
log('not support');
}
// not support
})(console.log, String)
上面代码中,由于string.matchAll(regex)
返回的是遍历器
,所以可以用for...of
循环取出。
相对于返回数组
,返回遍历器
的好处在于,如果匹配结果
是一个很大的数组
,那么遍历器
比较节省资源
。
遍历器
转为数组
是非常简单的,使用...
运算符和Array.from
方法就可以了。
(function (log, S) {
// g 修饰符加不加都可以
const regex = /t(e)(st(\d?))/g;
const string = 'test1test2test3';
if (string.matchAll) {
const matches = string.matchAll(regex);
// 转为数组方法一
log([...matches]);
// [ [ 'test1',
// 'e',
// 'st1',
// '1',
// index: 0,
// input: 'test1test2test3',
// groups: undefined ],
// [ 'test2',
// 'e',
// 'st2',
// '2',
// index: 5,
// input: 'test1test2test3',
// groups: undefined ],
// [ 'test3',
// 'e',
// 'st3',
// '3',
// index: 10,
// input: 'test1test2test3',
// groups: undefined ] ]
// 转为数组方法二
log(Array.from(matches));
// [ [ 'test1',
// 'e',
// 'st1',
// '1',
// index: 0,
// input: 'test1test2test3',
// groups: undefined ],
// [ 'test2',
// 'e',
// 'st2',
// '2',
// index: 5,
// input: 'test1test2test3',
// groups: undefined ],
// [ 'test3',
// 'e',
// 'st3',
// '3',
// index: 10,
// input: 'test1test2test3',
// groups: undefined ] ]
} else {
log('not support');
}
// not support
})(console.log, String)
传统的 JavaScript
语言,输出模板
通常是这样写的(下面使用了 jQuery
的方法)。
$('#result').append(
'There are <b>' + basket.count + '</b> ' +
'items in your basket, ' +
'<em>' + basket.onSale +
'</em> are on sale!'
);
之前写法相当繁琐
不方便,ES6
引入了模板字符串
解决这个问题
。
$('#result').append(`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`);
模板字符串(template string)
是增加版
的字符串
,用反引号
作为标识
,它可以当作普通字符串
使用,也可以用来定义多行字符串
。
使用模板字符串
表示多行字符串
,所有的空格
和缩进
都会被保留在输出
之中。
(function (log, S) {
// 普通字符串
log(`In JavaScript '\n' is a line-feed.`);
// In JavaScript '
// ' is a line-feed.
// 多行字符串
log(`In JavaScript this is
not legal.`);
// In JavaScript this is
// not legal.
log(`string text line 1
string text line 2`);
// string text line 1
// string text line 2
$('#list').html(`
<ul>
<li>first</li>
<li>second</li>
</ul>
`);
})(console.log, String)
代码
中的模板字符串
,都是用反引号
表示。
如果在模板字符串
中需要使用反引号
,则前面要用反斜杠
转义。
(function (log, S) {
let greeting = `\`Yo\` World!`;
log(greeting);// `Yo` World!
})(console.log, String)
模板字符串
中嵌入变量
,需要将变量名
写在${}
之中。
(function (log, S) {
// 字符串中嵌入变量
let name = "Bob",
time = "today";
log(`Hello ${name}, how are you ${time}?`);
// Hello Bob, how are you today?
function authorize(user, action) {
if (!user.hasPrivilege(action)) {
throw new Error(
// 传统写法为
// 'User '
// + user.name
// + ' is not authorized to do '
// + action
// + '.'
`User ${user.name} is not authorized to do ${action}.`);
}
}
})(console.log, String)
简单表达式
模板字符串
中大括号
内部可以放入任意的 JavaScript 表达式
,可以进行运算
,以及引用对象属性
。
(function (log, S) {
let x = 1;
let y = 2;
log(`${x} + ${y} = ${x + y}`);
// "1 + 2 = 3"
log(`${x} + ${y * 2} = ${x + y * 2}`);
// "1 + 4 = 5"
let obj = {
x: 1,
y: 2
};
log(`${obj.x + obj.y}`);
// "3"
})(console.log, String)
调用函数
模板字符串
之中还能调用函数
。
(function (log, S) {
function fn() {
return "Hello World";
}
log(`foo ${fn()} bar`);
// foo Hello World bar
})(console.log, String)
字符串变量
由于模板字符串
的大括号内部
,就是执行 JavaScript 代码
,因此如果大括号内部
是一个字符串
,将会原样输出
。
(function (log, S) {
let msg = `Hello ${'Melon'}`;
log(msg);// Hello Melon
})(console.log, String)
需要时执行
如果需要引用模板字符串
本身,在需要
时执行
,可以像下面这样写。
(function (log, S) {
// 写法一
let str1 = 'return ' + '`Hello ${name}!`';
let func1 = new Function('name', str1);
log(func1('Melon')); // "Hello Melon!"
// 写法二
let str2 = '(name) => `Hello ${name}!`';
let func2 = eval.call(null, str2);
log(func2('Melon')); // "Hello Melon!"
})(console.log, String)
如果大括号
中的值不是字符串
,将按照一般的规则
转为字符串
。
比如,大括号
中是一个对象
,将默认调用对象
的toString
方法。
(function (log, S) {
let a = [1,3,4];
let o = {melon:1};
let d = Reflect.construct(Date,[]);
log(`${a} + ${o} = ${d}`);
// 1,3,4 + [object Object] = Mon Feb 11 2019 23:45:23 GMT+0800 (GMT+08:00)
})(console.log, String)
如果模板字符串
中的变量
没有声明
,将报错
。
(function (log, S) {
// 变量place没有声明
let msg = `Hello, ${place}`;// ReferenceError: place is not defined
})(console.log, String)
模板字符串
可以嵌套
。
下面tmpl
方法中,模板字符串
的变量
之中,又嵌入了另一个模板字符串
。
(function (log, S) {
const tmpl = addrs => `
<table>
${addrs.map(addr => `
<tr><td>${addr.first}</td></tr>
<tr><td>${addr.last}</td></tr>
`).join('')}
</table>
`;
const data = [{
first: '<Jane>',
last: 'Bond'
},
{
first: 'Lars',
last: '<Croft>'
},
];
log(tmpl(data));
// <table>
//
// <tr><td><Jane></td></tr>
// <tr><td>Bond</td></tr>
//
// <tr><td>Lars</td></tr>
// <tr><td><Croft></td></tr>
//
// </table>
})(console.log, String)
const template = `
<ul>
<% for(let i=0; i < data.supplies.length; i++) { %>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`;
上面代码在模板字符串
之中,放置了一个常规模板
。
该模板使用<%...%>
放置 JavaScript
代码,使用<%= ... %>
输出 JavaScript
表达式。
将其转换为 JavaScript 表达式
字符串。
(function (log, S) {
function tmpl(data) {
const arr = [];
arr.push('<ul>');
for (let i = 0; i < data.supplies.length; i++) {
arr.push('\n\t<li>');
arr.push(data.supplies[i]);
arr.push('</li>');
};
arr.push('\n</ul>');
return arr.join('');
}
const data = {
supplies: ["broom", "mop", "cleaner"]
};
log(tmpl(data));
// <ul>
// <li>broom</li>
// <li>mop</li>
// <li>cleaner</li>
// </ul>
})(console.log, String)
使用正则表达式
将模板中<%= ... %>
转换为echo
字符串拼接方法,然后再结合模板字符串
,转译最后的编译方法完整体
,最后再调用eval
动态执行函数编译
。
(function (log, S) {
function compile(template) {
const evalExpr = /<%=(.+?)%>/g;
const expr = /<%([\s\S]+?)%>/g;
template = template.replace(evalExpr, '`); \n echo( $1 ); \n echo(`')
template = template.replace(expr, '`); \n $1 \n echo(`');
template = 'echo(`' + template + '`);';
let script =
`(function parse(data){
let output = "";
function echo(html){
output += html;
}
${ template }
return output;
})`;
return script;
}
const data = {
supplies: ["broom", "mop", "cleaner"]
};
const temp = `
<ul>
<% for(let i=0; i < data.supplies.length; i++) { %>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`;
// let tmpl = new Function(temp,compile);
const compileStr = compile(temp);
log(compileStr);
// (function parse(data) {
// let output = "";
// function echo(html) {
// output += html;
// }
// echo(`
// <ul>
// `);
// for (let i = 0; i < data.supplies.length; i++) {
// echo(`
// <li>`);
// echo(data.supplies[i]);
// echo(`</li>
// `);
// }
// echo(`
// </ul>
// `);
// return output;
// })
let tmpl = eval(compileStr);
log(tmpl(data));
// <ul>
// <li>broom</li>
// <li>mop</li>
// <li>cleaner</li>
// </ul>
})(console.log, String)
使用正则表达式
将模板中<%= ... %>
转换为arr.push
字符串拼接方法,然后再结合模板字符串
,转译最后的编译方法完整体
,最后再调用eval
动态执行函数编译
。
(function (log, S) {
function compile(template) {
const evalExpr = /<%=(.+?)%>/g;
const expr = /<%([\s\S]+?)%>/g;
template = template.replace(evalExpr, '`); \n arr.push( $1 ); \n arr.push(`');
// 替换模板字符串中的<%=...%>中的值
// 比如将 '<%= data.supplies[i] %>'
// 转换为 '`);\n arr.push( data.supplies[i]); \n arr.push(`'
template = template.replace(expr, '`); \n $1 \n arr.push(`');
// 替换模板字符串中的<%...%>中的值
// 比如将 '<% for(let i=0; i < data.supplies.length; i++) { %>'
// 转换为 '`);\n for(let i=0; i < data.supplies.length; i++) \n arr.push(`'
template = 'arr.push(`' + template + '`);'; // 做最外层的包裹
let script =
`(function parse(data){
const arr = [];
${ template }
return arr.join('');
})`;
return script;
}
const data = {
supplies: ["broom", "mop", "cleaner"]
};
const temp = `
<ul>
<% for(let i=0; i < data.supplies.length; i++) { %>
<li><%= data.supplies[i] %></li>
<% } %>
</ul>
`;
// let tmpl = new Function(temp,compile);
const compileStr = compile(temp);
log(compileStr);
// (function parse(data){
// const arr = [];
// arr.push(`
// <ul>
// `);
// for(let i=0; i < data.supplies.length; i++) {
// arr.push(`
// <li>`);
// arr.push( data.supplies[i] );
// arr.push(`</li>
// `);
// }
// arr.push(`
// </ul>
// `);
// return arr.join('');
// })
let tmpl = eval(compileStr);
log(tmpl(data));
// <ul>
// <li>broom</li>
// <li>mop</li>
// <li>cleaner</li>
// </ul>
})(console.log, String)
前面提到标签模板
里面,可以内嵌其他语言
。
但是,模板字符串
默认会将字符串转义
,导致无法嵌入其他语言
。
举例来说,标签模板里面可以嵌入 LaTEX
语言。
function latex(strings) {
// ...
}
let document = latex`
\newcommand{\fun}{\textbf{Fun!}} // 正常工作
\newcommand{\unicode}{\textbf{Unicode!}} // 报错
\newcommand{\xerxes}{\textbf{King!}} // 报错
Breve over the h goes \u{h}ere // 报错
`
上面代码中,变量document
内嵌的模板字符串
,对于 LaTEX
语言来说完全是合法
的。
但是 JavaScript
引擎会报错。原因就在于字符串
的转义
。
模板字符串
会将\u00FF
和\u{42}
当作 Unicode
字符进行转义,所以\unicode
解析时报错;
而\x56
会被当作十六进制
字符串转义,所以\xerxes
会报错。
也就是说,\u
和\x
在 LaTEX
里面有特殊含义
,但是 JavaScript
将它们转义了。
为了解决这个问题,ES2018
放松了对标签模板
里面的字符串转义
的限制。
如果遇到不合法
的字符串转义
,就返回undefined
,而不是报错,并且从raw
属性上面可以得到原始字符串
。
function tag(strs) {
strs[0] === undefined
strs.raw[0] === "\\unicode and \\u{55}";
}
tag`\unicode and \u{55}`
上面代码中,模板字符串
原本是应该报错的,但是由于放松
了对字符串转义
的限制
,所以不报错了,JavaScript
引擎将第一个字符设置为undefined
/
但是raw
属性依然可以得到原始字符串
,因此tag
函数还是可以对原字符串
进行处理。
注意,这种对字符串转义
的放松
,只在标签模板解析
字符串时生效,不是标签模板
的场合,依然会报错
。
let bad = `bad escape sequence: \unicode`; // 报错
ES6
为字符串
添加了遍历器
接口(详见《Iterator》
一章),使得字符串
可以被for...of
循环遍历。
(function (log, S) {
for (let codePoint of 'foo') {
log(codePoint)
}
// "f"
// "o"
// "o"
})(console.log, String)
除了遍历字符串,这个遍历器
最大的优点是可以识别大于0xFFFF
的码点
,传统的for循环
无法识别这样的码点
。
(function (log, S) {
const text = String.fromCodePoint(0x20BB7);
for (let i = 0; i < text.length; i++) {
log(text[i]);
}
// �
// �
for (let i of text) {
log(i);
}
// "𠮷"
})(console.log, String)
上面代码中,字符串text
只有一个字符
,但是for
循环会认为它包含两个字符
(都不可打印),而for...of
循环会正确识别
出这一个字符
。
模板字符串
的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串
。这被称为标签模板
功能(tagged template
)。
alert`123`
// 等同于
alert(123)
标签模板
其实不是模板
,而是函数调用
的一种特殊形式
。标签
指的就是函数
,紧跟在后面的模板字符串
就是它的参数
。
但是,如果模板字符
里面有变量
,就不是简单的调用
了,而是会将模板字符串
先处理成多个参数
,再调用函数
。
let a = 5;
let b = 10;
tag`Hello ${ a + b } world ${ a * b }`;
// 等同于
tag(['Hello ', ' world ', ''], 15, 50);
上面代码中,模板字符串前面有一个标识名tag
,它是一个函数
。
整个表达式的返回值,就是tag
函数处理模板字符串后的返回值
。
函数tag
依次会接收到多个参数。
function tag(stringArr, value1, value2){
// ...
}
// 等同于
function tag(stringArr, ...values){
// ...
}
tag
函数的第一个参数
是一个数组
,该数组
的成员
是模板字符串
中那些没有变量替换
的部分。
也就是说,变量替换
只发生在数组
的第一个成员
与第二个成员
之间、第二个成员
与第三个成员
之间,以此类推。
tag
函数的其他参数
,都是模板字符串
各个变量被替换
后的值
。
由于本例中,模板字符串含有两个变量,因此tag
会接受到value1
和value2
两个参数。
tag
函数所有参数
的实际值
如下。
参数顺序标识 | 参数值说明 |
---|---|
第一个参数 | ['Hello ', ' world ', ''] |
第二个参数 | 15 |
第三个参数 | 50 |
也就是说,tag
函数实际上以下面的形式调用。
tag(['Hello ', ' world ', ''], 15, 50)
我们可以按照需要编写tag
函数的代码。下面是tag
函数的一种写法,以及运行结果。
let a = 5;
let b = 10;
function tag(s, v1, v2) {
console.log(s[0]);
console.log(s[1]);
console.log(s[2]);
console.log(v1);
console.log(v2);
return "OK";
}
tag`Hello ${ a + b } world ${ a * b}`;
// "Hello "
// " world "
// ""
// 15
// 50
// "OK"
下面是一个更复杂
的例子。
let total = 30;
let msg = passthru`The total is ${total} (${total*1.05} with tax)`;
function passthru(literals) {
let result = '';
let i = 0;
while (i < literals.length) {
result += literals[i++];
if (i < arguments.length) {
result += arguments[i];
}
}
return result;
}
msg // "The total is 30 (31.5 with tax)"
上面这个例子展示了,如何将各个参数按照原来的位置拼合回去。
passthru
函数采用 rest
参数的写法如下。
function passthru(literals, ...values) {
let output = "";
let index;
for (index = 0; index < values.length; index++) {
output += literals[index] + values[index];
}
output += literals[index]
return output;
}
标签模板
的一个重要应用,就是过滤 HTML
字符串,防止用户输入恶意内容。
let message =
SaferHTML`<p>${sender} has sent you a message.</p>`;
function SaferHTML(templateData) {
let s = templateData[0];
for (let i = 1; i < arguments.length; i++) {
let arg = String(arguments[i]);
// Escape special characters in the substitution.
s += arg.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
// Don't escape special characters in the template.
s += templateData[i];
}
return s;
}
上面代码中,sender
变量往往是用户提供的,经过SaferHTML
函数处理,里面的特殊字符都会被转义。
let sender = '<script>alert("abc")</script>'; // 恶意代码
let message = SaferHTML`<p>${sender} has sent you a message.</p>`;
message
// <p><script>alert("abc")</script> has sent you a message.</p>
标签模板的另一个应用,就是多语言转换(国际化处理)。
i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!`
// "欢迎访问xxx,您是第xxxx位访问者!"
模板字符串本身并不能取代 Mustache
之类的模板库,因为没有条件判断
和循环
处理功能,但是通过标签函数
,你可以自己添加这些功能。
// 下面的hashTemplate函数
// 是一个自定义的模板处理函数
let libraryHtml = hashTemplate`
<ul>
#for book in ${myBooks}
<li><i>#{book.title}</i> by #{book.author}</li>
#end
</ul>
`;
除此之外,你甚至可以使用标签模板
,在 JavaScript
语言之中嵌入其他语言
。
jsx`
<div>
<input
ref='input'
onChange='${this.handleChange}'
defaultValue='${this.state.value}' />
${this.state.value}
</div>
`
上面的代码通过jsx
函数,将一个 DOM
字符串转为 React
对象。你可以在 GitHub
找到jsx
函数的具体实现。
下面则是一个假想的例子,通过java
函数,在 JavaScript
代码之中运行 Java
代码。
java`
class HelloWorldApp {
public static void main(String[] args) {
System.out.println("Hello World!"); // Display the string.
}
}
`
HelloWorldApp.main();
模板处理函数
的第一个参数
(模板字符串
数组),还有一个raw
属性。
console.log`123`
// ["123", raw: Array[1]]
上面代码中,console.log
接受的参数,实际上是一个数组
。
该数组
有一个raw属性
,保存的是转义
后的原字符串
。
请看下面的例子。
tag`First line\nSecond line`
function tag(strings) {
console.log(strings.raw[0]);
// strings.raw[0] 为 "First line\\nSecond line"
// 打印输出 "First line\nSecond line"
}
上面代码中,tag
函数的第一个参数strings
,有一个raw
属性,也指向一个数组
。
该数组
的成员
与strings数组
完全一致。
比如,strings数组
是["First line\nSecond line"]
,那么strings.raw
数组就是["First line\\nSecond line"]
。
两者唯一的区别,就是字符串里面的斜杠
都被转义
了。
比如,strings.raw
数组会将\n
视为\\
和n
两个字符
,而不是换行符
。
这是为了方便取得转义
之前的原始模板
而设计
的。
ES6提供了二进制和八进制数值的新写法,分别用前缀0b
和0o
表示。
0b11111011 === 503 // true
0o767 === 503 // true
八进制用0o前缀表示的方法,将要取代已经在ES5中被逐步淘汰的加前缀0的写法。
ES6自Number对象上,新提供了Number.isFinite()和Number.isNaN(),两个方法,用来检查Infinite和NaN这两个特殊值。
与传统的isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,对于非数值一律返回false。
将全局方法parseInt()
和parseFloat()
,移植到了Number对象上面,行为完全保持不变,这样做的目的,是逐步减少全局性方法,使语言逐步模块化。
Number.isInteger()用来判断一个值是否为整数,需要注意的是,在Javascript内部,整数和浮点数使用同样的存储方法,所以3和3.0被视为同一个值。
Number.isSafeInteger()则用来判断一个整数是否落在这个范围之内。
Javascript能够准确表示的整数范围为-2^53 ~ 2^53。ES6引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。
方法名 | 方法描述 |
---|---|
Math.trunc() | 用于去除一个数值的小数部分,返回其整数部分,注意这个方法不会做四舍五入,返回的是未修改数值整数部分。 |
Math.asinh(x) | 返回x的反双曲正弦 |
Math.acosh(x) | 返回x的反双曲余弦 |
Math.atanh(x) | 返回x的反双曲正切 |
Math.sin(x) | 返回x的双曲正弦 |
Math.cosh(x) | 返回x的双曲余弦 |
Math.tanh(x) | 返回x的双曲正切 |
Math.cbrt(x) | 返回x的立方根 |
Math.clz32(x) | 返回x的32位二进制整数表示形式的前导0的个数 |
Math.expml(x) | 返回e^x -1 |
Math.found(x) | 返回x的单精度浮点数形式 |
Math.hypot(...values) | 返回所有参数的平方和的平方根 |
Math.imul(x,y) | 返回两个参数以32位整数形式相乘的结果 |
Math.loglp(x) | 返回 ln(1+x) |
Math.log10(x) | 返回 以10为底的对数,lg x |
Math.log2(x) | 返回 以2为底的对数,log2 x |
方法名 | 方法描述 |
---|---|
Array.form(arrLikeObj,dealItemFunc) | 用于将两类对象转换为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象,其中包括ES6新增的Set和Map结构。 |
Array.of | 用于将一组值转换为数组,主要是弥补数组构造函数Array()不足。因为参数个数的不同,会导致Array()的行为有差异,因为Array()的参数不少于2个,才会返回所提供参数组组成的新数组 |
Array.from(arrayLike,x => x % x);
// 等同于
Array.from(arrayLike).map(x => x % x);
Array.of(3,11,8) // [3,11,8]
Array.of(3).length // 1
Array(3).length // 3
方法名 | 方法描述 |
---|---|
find(findFunc[,bindThis]) | 用于找到第一个符合条件的数组元素。findFunc(value,index,arr) 是一个回调函数,所有数组元素依次遍历该回调函数,直到找出第一个返回值为true的元素,然后返回该元素,反之返回 undefined,支持对于NaN的检测,从而弥补IndexOf()的不足 |
findIndex(findFunc[,bindThis]) | 用于找到第一个符合条件的数组元素位置。findFunc(value,index,arr) 是一个回调函数,所有数组元素依次遍历该回调函数,直到找出第一个返回值为true的元素,然后返回该元素位置,反之返回 -1,支持对于NaN的检测,从而弥补IndexOf()的不足 |
fill(fillNum[,fillStartNum,fillEndNum]) | 给定值填充一个数组,用于空数组的初始化非常方便。如果未指定开始和结束索引,数组中已有的元素,会被全部抹去 |
[NaN].indexOf(NaN)// -1
[NaN].findIndex(x => Object.is(NaN,y))
[1,2,3].fill(4,1,2) // [1,4,3]
ES6中每一个数组实例对象,都有entries()
,keys()
和values()
等三个新方法,都是返回的一个遍历器,可以用for...of
循环进行遍历,entries()
是对键值对的遍历,keys()
是对键名的遍历和values()
是对键值的遍历。
允许直接通过现有数组生成新数组,称为数组推导,for...of结构总是写在最前面,返回的表达式写在最后面,可以用于代替map方法。
for...of 后面还可以附加if语句,用来设定循环的限制条件,并且同时可以使用多个,可以用于代替 filter方法。
需要注意的是,数组推导的方括号构成一个单独的作用域,在这个方括号中声明的变量类似于使用let语句声明的变量。
由于字符串可以视为数组,因此字符串也可以直接用于数组推导。
关于数组推导需要注意的地方是,新数组会立即在内存中生成。这时如果原数组是一个很大的数组,将会非常耗费内存。
const a1 = [1,2];
const a2 = [for (i of a1) i * 2]; // [2,4]
const a1 = [1954,1974,1990,2006,2010,2014];
const a2 = [for (year of a1) if(year > 2000) year]; // [2006,2010,2014]
const a3 = [for (year of a1) if(year > 2000) if(year < 2010) year]; // [2006]
const a1 = [1,2];
const a2 = [10,20];
const a3 = [11,22];
const a4 = [for (a of a1) for(b of a2) for(c of a3) c-b-a]; // [0,0]
const s1 = [for (c of 'abcde' c+'0')].join('')// 'a0b0c0d0e0'
这两个方法用于监听(取消)监听数组的变化,指定回调函数,这个现在好像已经废弃了。
方法名 | 方法描述 |
---|---|
Object.is(compareA,compareB) | 用来比较两个值是否严格相等,它与严格比较运算符(===)的行为基本一致,不同之处有两点,一个是+0不等于-0,二是两个NaN相等。 |
Object.assign(targetObj,...sourceObj) | 用于将源对象(source)的所有可枚举属性,复制都目标对象(target),第一个参数是目标对象,后面的参数都是源对象,只要有一个参数不是对象,就会抛出异常,如果目标对象与(多个)源对象有同名属性,则后面的属性会覆盖前面的属性 |
Object.setPrototypeOf(object,prototype) | 作用与proto相同,用来设置一个对象的prototype对象 |
Object.getPrototypeOf(obj) | 用来读取一个对象的prototype对象 |
Object.setPrototypeOf({},null);
Object.setPrototypeOf({});
ES6允许直接写入变量和函数,作为对象的属性和方法,这样的写法更加简洁。
这种写法用于函数的返回值,将会非常方便。
const Person = {
name:'melon',
birth,// 类似 birth:birth
hello(){}// 类似 hello:function(){}
};
function getPoint(){
const a = 1;
const b = 2 ;
return {a,b};
}
getPoint();// {a:1,b:2}
ES6允许定义对象时,用表达式作为对象的属性名,在写法上,要把表达式放在方括号内。
const lastWord = 'last word';
const a = {
'first word':'hello',
[lastWord]:'melon'
};
const suffix = ' word';
const b = {
['first'+suffix]:'hello',
['last'+suffix]:'melon'
};
ES6引入了一种新的原始数据类型Symbol,它通过Symbol函数生成,
Symbol函数接受一个字符串作为参数,用来指定生成的Symbol的名称,可以通过那么属性读取。typeof运算符的结果,表明Symbol是一种单独的数据类型。
注意,Symbol函数前不能使用new命令,否则会报错,这是因为生成的Symbol是一个原始类型的值,不是对象。
Symbol最大的特点,就是每一个Symbol都是不相等的,保证产生一个独一无二的值。所以Symbol类型适合作为标识符,用于对象的属性名,保证s属性名之间不发生冲突,如果一个对象由多个模块构成,这样就不会出现同名的属性。
Symbol类型作为属性名,可以被遍历,Object.getOwnPropertySymbols()和Object.getOwnPropertyKeys(),都可以获取该属性。
const mySymbol = Symbol('Test');
mySymbol.name // Test
typeof mySymbol // symbol
const w1 = Symbol();
const w2 = Symbol();
const w3 = Symbol();
// 普通写法
const mySymbol = Symbol();
const a = {
[mySymbol]:'Hello'
};
// define写法
Object.defineProperty({},mySymbol,{value:'Hello'})
可以理解为在目标对象之前,架设一层'拦截器',外界对该对象的访问,都需要先通过这层拦截,可以被过滤和改写。
可以用于异常属性获取时,及时抛出异常。
const proxy = new Proxy({},{
get(target,property){
if(property in target){
return target[property];
} else{
throw new ReferenceError('blabla')
}
}
})
ES6 允许为函数的参数设置默认值,任何带有默认值的参数,都会被视为可选参数。不带默认值的参数视为必须参数
利用参数默认值,可以指定摸一个参数不得省略,如果省略就抛出一个错误。
function throwIfMissing(){
throw new Error('Missing parameter');
}
function foo(mustBeProvided = throwIfMissing()){
return mustBeProvided;
}
foo();// Error:Missing parameter
ES6引入了rest参数(...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了,rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
数组特有的方法都可以用于这个变量。
注意,rest参数之后不能再有其他参数,否则会报错。
扩展运算符(spread) 是三个点(...),它好比rest参数的逆运算,将一个数组转换为用逗号分隔的参数序列,该运算符主要用于函数调用。
扩展运算符,可以简化求出一个数组最大元素的写法。
扩展运算符可以简化数组和对象的赋值。
Math.max.apply(nul,[5,9,0]);
Math.max(...[5,9,0]);
const a1 = [1];
const a2 = [2];
const a3 = [3];
const a4 = [0,..a1,...a2,...a3];
const o1 = {a:1};
const o2 = {b:2};
const o3 = {c:3};
const o4 = {..o1,...o2,...o3};
ES6允许使用 箭头(=>)定义函数,可以用于简化回调函数。
如果箭头函数不需要参数或需要多个参数,就使用一对圆括号代表参数部分。
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
由于大括号被解释为代码块,因而如果箭头函数直接返回一个对象,必须在对象外面加上括号。
const getTempItem = id => ({id,name:'Temp’});
注意点
ES6提供新的数据结构Set(注意和原始数据类型的不同之处),它类似于数组,只不过它生成的值是用大括号包裹的,并且其成员数都是唯一的,没有重复的值,
Set本身时一个构造函数,用来生成Set数据结构。
Set函数可接受一个数组作为参数,用来初始化。
向Set加入值的时候,不会发生类型转换,这意味着,在Set中7和'7'时两个不同的值。
Set结构的对象实例化之后,有两个属性construcitor
和size
,前者用于返回当前实例对象的构造函数,默认就是Set函数。后者时返回Set成员总数。
实例方法名称 | 方法说明 |
---|---|
add(value) | 返回的值是当前已经添加过成员之后的Set对象,添加某个值到实例对象中,注意该方法可以存在链式调用 |
delete(value) | 删除实例对象中某个值。返回一个布尔值,表示是否删除成功。 |
has(value) | 返回一个布尔值,表示该值是否为Set的成员 |
clear() | 清除所有成员 |
目前使用场景场景比较多的情况下是利用Set对象,做数组去重,先将数组值传递个Set构造函数,生成Set对象,自动去重,然后再通过Array.from
再转换为数组对象进行返回。
((log) => {
const items = new Set([2, 2, 2, 2, 2, 2, 2, 6]);
log(items);//Set {2,6}
log(items.add(3).add(9));// Set {2,6,3,9}
log(items.has(2));// true
log(items.has(3));// true
log(items.delete(2));// true
log(items.delete(4));// false
log(items.has(2));// false
log(items.clear());// undefined
log(items);// Set {}
log(items.size);// 0
})(console.log)
function dedupe(array){
return Array.from(new Set(array));
}
Javascript的对象,本质上键值对的集合,但是只能用字符串当作键,这给它的使用带来了很大的限制。
如果在原始对象中将一个非字符串类型的值作为键名,则内部机制会自动隐式转换为一个字符串的值,类似于自动调用对象的toString
方法,在作为对象存储值的存储键名。
为了解决这个问题,ES6提供了Map结构,它类似于对象,也是键值对的集合,但是键名的范围不限于字符串,对象也可以当作键名。
Map函数也可以接受一个二维数组进行数据初始化,默认数组中索引为0的为存储的键名,索引为1的为存储的键值,如果初始化的数据中,存在值和内存地址都相同的键名,则默认只保留最后一个的存储键值对。
Map结构的对象实例化之后,有两个属性construcitor
和size
,前者用于返回当前实例对象的构造函数,默认就是Map函数。后者时返回Map成员总数。
实例方法名称 | 方法说明 |
---|---|
set(name,value) | 返回的值是当前已经添加过成员之后的Map对象,添加某个值到实例对象中,注意该方法可以存在链式调用 |
get(name) | 判断当前实例对象中是否含有某个键名为name的键值。如果在当前实例对象中能够正常取到对应的键值,则返回对应的键值,反之,则返回undefined |
delete(value) | 删除实例对象中某个值。返回一个布尔值,表示是否删除成功。 |
has(value) | 返回一个布尔值,表示该值是否为Map的成员 |
clear() | 清除所有成员,不返回任何值 |
((log) => {
const m1 = new Map();
log(m1);//Map {}
log(m1.size);// 0
const o = {22:'aa'};
log(m1.set(o,'melon'));// Map {{22:'aa'} =>'melon'}
log(m1);// Map {{22:'aa'} =>'melon'}
log(m1.size);// 1
log(m1.get(o));// melon
log(m1.delete(o));// true
const m2 = new Map([[11,22],[33,44]]);
log(m2);// Map {11=>22,33=>44}
log(m2.size);// 2
log(m2.has(11));// true
log(m2.get(11));// 22
log(m2.has(33));// true
log(m2.get(33));// 44
log(m2.get(44));// undefined
log(m2.clear());// undefined
log(m2);// Map {}
log(m2.size);// 0
const m3 = new Map([[11,22],[11,44]]);
log(m3);// Map {11=>44}
log(m3.size);// 1
const m4 = new Map([[[11],22],[[11],44]]);
log(m4);// Map {[11]=>22,[11]=>44}
log(m4.size);// 2
})(console.log)
Map对象提供三个遍历器
遍历器方法名 | 遍历器方法说明 |
---|---|
keys() | 返回键名的遍历器 |
values() | 返回键值的遍历器 |
entries() | 返回所有成员的遍历器 |
((log) => {
const m1 = new Map([[11, 22], [33, 44]]);
log(m1);//Map {11 =>22,33=>44}
log(m1.size);// 2
for (let key of m1.keys()) {
log(`Key : ${key}`);
}
// Key : 11
// Key : 33
for (let val of m1.values()) {
log(`Value : ${val}`);
}
// Value : 22
// Value : 44
for (let item of m1.entries()) {
log(`Key : ${item[0]},Value : ${item[1]}`);
}
// Key : 11,Value : 22
// Key : 33,Value : 44
m1.forEach((value, key, map) => {
log(`Key : ${key},Value : ${value}`);
});
// Key : 11,Value : 22
// Key : 33,Value : 4
const reporter = {
report: function (key, value) {
log(`Key : ${key},Value : ${value}`);
}
};
// m1.forEach((value, key, map) => {
// this.report(key,value);
// },reporter);
// 这样指定this对象,再使用对应方法会报错
// 注意不要使用箭头函数,这样第二个辅助参数挟持的this会失效,导致使用手动绑定对象方法会报错。
m1.forEach(function(value, key, map) {
this.report(key,value);
},reporter);
// Key : 11,Value : 22
// Key : 33,Value : 44
})(console.log)
其实应该是在2019农历,过年前完成的,但是各种事,都到新的一年了,还有几章没有完成,日常捂脸.jpg。
bad
开始接触es6有好几年了,但是一直没系统的学习,这边是在家弱网的情况下,开始系统的看,感觉阮大在14年就写的东西,我现在看,仍然可以看的一些新用法,就感觉就自己学习过于浮于表面的感觉。
而且当时是2014年,很多人对于前端都是觉得只是辅助的时候,当时是jq一梭哈的时候,这个时候在没有浏览器厂商支持的情况下,编写完这本书,感觉很厉害。
而且书中作序有提到,希望我们中国人声音能够被听到,能够参与到前端js的规范定制中,只有我们对于每个提案都更好的关注,有理有据的提出我们的建议,让国际知晓我们的声音。
2014年到现在2019年,五年过去了,ES7 ES8都已经有浏览器厂商实现部分功能了,但是感觉自己项目组中,也就2018年年末才开始有一些人开始了解es6,更多的是对js语法更替的忽视和抗拒,感觉环境也有些改变我了。
大前端口号喊了许久,而我还处于农村喜通网的状态,感觉要加快进度了。
good
当时阮大写书的时候,各大浏览器厂商都没怎么实现,现有目前很多功能都以及被实现。
当时babel规范还不太流行,我习惯了babel规范,再来看代码,会有一些觉得可以改进的地方。
在阅读前几章过程中,可以做到更少的代码描述和文字截取,因为已经有其他更好的关于这个知识点体系的思维逻辑,在这边只用来做梳理,甚至可以做相关纠错,这种感觉就有些赞。
在梳理文章的过程中,学习了一下阮大的分类习惯,按照自己的习惯重新分了一遍,自我感觉良好,日常捂脸.jpg。
算是送给自己的生日礼物吧
总要有一个坑 自己能够填完
第一章 ECMAScript 6 简介
ES6
的目标,是使得JavaScript
语言可以用来编写大型的复杂应用程序,成为企业级开发语言。ECMAScript
是JavaScript
语言的国际标准,JavaScript
是ECMAScript
的实现。第一小节 基础前置知识
1.1 ECMAScript的历史
1996年11月,
JavaScript
的创造者Netscape
公司,决定将JavaScript
提交给国际标准化组织ECMA
,希望这种语言能够成为国际标准。次年,ECMA
发布262
号标准文件(ECMA-262
)的第一版
,规定了浏览器脚本语言的标准,并将这种语言成为ECMAScript
。这个版本就是ECMAScript 1.0
版。1998年6月,
ECMAScript 2.0
版发布。1999年12月,
ECMAScript 3.0
版发布,成为JavaScript
大的通行标准,得到了广泛支持。3.0
版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了JavaScript
语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习JavaScript
,其实就是在学3.0
版的语法。2000 年,
ECMAScript 4.0
开始酝酿。这个版本最后没有通过,但是它的大部分内容被ES6
继承了。因此,ES6
制定的起点其实是2000
年。2007年10月,
ECMAScript 4.0
版 草案发布,对3.0
版做了大幅升级,原计划次年8月发布正式版本。然而在草案发布后,由于4.0
版的目标过于激进,各方对于是否通过这个标准,产生了严重分歧。以Yahoo
,Microsoft
,Google
为首的大公司,反对JavaScript
的大幅升级,主张小幅改动,而以JavaScript
创造者Brendan Eich
为首的Mozilla
公司,则坚持当前的草案。2008年7月,由于对于 下一个版本应该包含哪些功能,各方面分歧太大,争论过于激进,
ECMA
开会决定,中止ECMAScript 4.0
的开发,将其中设计现有功能改善的一小部分,发布为ECMAScript 3.1
,二将其他激进的设想扩大范围,放入以后的版本,鉴于会议的气氛,该版本的项目代号取名为Harmony
(和谐),会后不久,ECMAScript 3.1
就改名为ECMAScript 5
。2009年12月,
ECMAScript 5.0
版正式发布。Harmony
项目则一分为二,一些较为可行的设想定名为JavaScript.next
继续开发,后来演变成ECMAScript 6
,一些不是很成熟的设想,则被视为JavaScript.next.next
,在更远的将来再考虑推出。2011年6月,
ECMAScript 5.1
版本发布,并且成为ISO
国际标准(ISO/IEC 16262:2011
)。2013年3月,
ECMAScript 6
草案冻结,不再添加新功能,新功能设想将被放到ECMAScript 7
。2013年12月,
ECMAScript 6
草案发布,此后是12个月的讨论期,以听取各方反馈意见。2015年6月,
ES6
的第一个版本发布,正式名称就是《ECMAScript 2015 标准》
(简称ES2015
)。2016年6月,小幅修订的
《ECMAScript 2016 标准》
(简称ES2016
)如期发布。1.2 与 JavaScript 的关系
一个常见的问题是,
ECMAScript
和JavaScript
到底是什么关系?要讲清楚这个问题,需要回顾历史。1996 年 11 月,
JavaScript
的创造者Netscape
公司,决定将JavaScript
提交给标准化组织ECMA
,希望这种语言能够成为国际标准。次年,ECMA
发布262
号标准文件(ECMA-262
)的第一版,规定了浏览器脚本语言
的标准,并将这种语言称为ECMAScript
,这个版本就是1.0
版。该标准从一开始就是针对
JavaScript
语言制定的,但是之所以不叫JavaScript
,有两个原因。一是商标,Java 是 Sun 公司的商标,根据授权协议,只有Netscape
公司可以合法地使用JavaScript
这个名字,且JavaScript
本身也已经被Netscape
公司注册为商标。二是想体现这门语言的制定者是ECMA
,不是Netscape
,这样有利于保证这门语言的开放性
和中立性
。因此,
ECMAScript
和JavaScript
的关系是,前者是后者的规格,后者是前者的一种实现(另外的ECMAScript
方言还有JScript
和ActionScript
)。日常场合,这两个词是可以互换的。1.3 与 ES2015 的关系
ECMAScript 2015
(简称ES2015
)这个词,也是经常可以看到的。它与ES6
是什么关系呢?2011
年,ECMAScript 5.1
版发布后,就开始制定6.0
版了。因此,ES6
这个词的原意,就是指JavaScript
语言的下一个版本。但是,因为这个版本引入的语法功能太多,而且制定过程当中,还有很多组织和个人不断提交新功能。事情很快就变得清楚了,不可能在一个版本里面包括所有将要引入的功能。常规的做法是先发布
6.0
版,过一段时间再发6.1
版,然后是6.2
版、6.3
版等等。但是,标准的制定者不想这样做。他们想让标准的升级成为常规流程:任何人在任何时候,都可以向标准委员会提交新语法的提案,然后标准委员会每个月开一次会,评估这些提案是否可以接受,需要哪些改进。
如果经过多次会议以后,一个提案足够成熟了,就可以正式进入标准了。这就是说,标准的版本升级成为了一个不断滚动的流程,每个月都会有变动。
标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。
接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。
ES6
的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015
标准》(简称ES2015
)。2016 年 6 月,小幅修订的《ECMAScript 2016
标准》(简称ES2016
)如期发布,这个版本可以看作是ES6.1
版,因为两者的差异非常小(只新增了数组实例的includes方法和指数运算符),基本上是同一个标准。根据计划,2017
年6
月发布ES2017
标准。因此,
ES6
既是一个历史名词,也是一个泛指,含义是5.1
版以后的JavaScript
的下一代标准,涵盖了ES2015
、ES2016
、ES2017
等等,而ES2015
则是正式名称,特指该年发布的正式版本的语言标准。本书中提到ES6
的地方,一般是指ES2015
标准,但有时也是泛指下一代 JavaScript 语言
。1.4 语法提案的批准流程
任何人都可以向标准委员会(又称
TC39
委员会)提案,要求修改语言标准。一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由
TC39
委员会批准。一个提案只要能进入
Stage 2
,就差不多肯定会包括在以后的正式标准里面。ECMAScript
当前的所有提案,可以在TC39
的官方网站gitHub.com/tc39/ecma262
查看。本书的写作目标之一,是跟踪
ECMAScript
语言的最新进展,介绍5.1
版本以后所有的新语法。对于那些明确或很有希望,将要列入标准的新语法,都将予以介绍。1.5 ES6支持情况
各大浏览器的最新版本,对
ES6
的支持可以查看kangax.github.io/compat-table/es6/
。随着时间的推移,支持度已经越来越高了,超过90%
的ES6
语法特性都实现了。Node
是JavaScript
的服务器运行环境(runtime
)。它对ES6
的支持度更高。除了那些默认打开的功能,还有一些语法功能已经实现了,但是默认没有打开。使用下面的命令,可以查看Node
已经实现的ES6
特性。我写了一个工具 ES-Checker,用来检查各种运行环境对
ES6
的支持情况。访问ruanyf.github.io/es-checker
,可以看到您的浏览器支持ES6
的程度。运行下面的命令,可以查看你正在使用的Node
环境对ES6
的支持程度。第二小节 通过Babel使用ES6
2.1 不同环境下使用babel
2.1.1 node环境的用法
Babel
是一个广泛使用的ES6
转码器,可以将ES6
代码转为ES5
代码,从而在现有环境执行。这意味着,你可以用
ES6
的方式编写程序,又不用担心现有环境是否支持。下面是一个例子。上面的原始代码用了箭头函数,
Babel
将其转为普通函数,就能在不支持箭头函数的JavaScript
环境执行了。下面的命令在项目目录中,安装
Babel
。Babel 6
的配置文件是.babelrc
,存放在项目的根目录下。使用Babel 6
及以下版本 的第一步,就是配置这个文件。该文件用来设置转码规则和插件,基本格式如下。
presets
字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。然后,将这些规则加入
.babelrc
。注意,以下所有
Babel 6
工具和模块的使用,都必须先写好.babelrc
。Babel 7
的配置文件是babel.config.js
,存放在项目的根目录下。使用Babel 7
及以上版本 的第一步,就是配置这个文件。该文件用来设置转码规则和插件,基本格式如下。
注意,以下所有
Babel 7
及以上版本 工具和模块的使用,都必须先写好babel.config.js
。更多配置
2.1.2 命令行转换
Babel
提供命令行工具@babel/cli
,用于命令行转码。它的安装命令如下。
基本用法如下
2.1.3 浏览器环境
Babel
也可以用于浏览器环境,使用[@babel/standalone](https://babeljs.io/docs/en/next/babel-standalone.html)
模块提供的浏览器版本,将其插入网页。注意,网页实时将
ES6
代码转为ES5
,对性能会有影响。生产环境需要加载已经转码完成的脚本。2.1.4 在线转换
Babel
提供一个REPL 在线编译器,可以在线将ES6
代码转为ES5
代码。转换后的代码,可以直接作为ES5
代码插入网页运行。2.2 babel功能说明
2.2.1 babel API
如果某些代码需要调用
Babel
的API
进行转码,就要使用@babel/core模块。配置对象
options
,可以参看官方文档http://babeljs.io/docs/usage/options/
。下面是一个例子。
上面代码中,
transform
方法的第一个参数是一个字符串
,表示需要被转换的ES6 代码
,第二个参数是转换的配置对象
。2.2.2 @babel/polyfill
Babel
默认只转换新的JavaScript
句法(syntax
),而不转换新的API
。比如
Iterator
、Generator
、Set
、Map
、Proxy
、Reflect
、Symbol
、Promise
等全局对象,以及一些定义在全局对象上的方法(比如Object.assign
)都不会转码。举例来说,
ES6
在Array
对象上新增了Array.from
方法。Babel
就不会转码这个方法。如果想让这个方法运行,必须使用
babel-polyfill
,为当前环境提供一个垫片。
安装命令如下。
然后,在脚本头部,加入如下一行代码。
Babel
默认不转码的 API 非常多,详细清单可以查看babel-plugin-transform-runtime
模块的[definitions.js](https://github.com/babel/babel/blob/master/packages/babel-plugin-transform-runtime/src/definitions.js)
文件。2.2.3 @babel/register
@babel/register
模块改写require
命令,为它加上一个钩子
。此后,每当使用require
加载.js
、.jsx
、.es
和.es6
后缀名的文件,就会先用Babel
进行转码。使用时,必须首先加载
@babel/register
。然后,就不需要手动对
index.js
转码了。需要注意的是,
@babel/register
只会对requir
e命令加载的文件转码,而不会对当前文件转码。另外,由于它是实时转码
,所以只适合在开发环境
使用。2.2.4 babel-node
@babel/node
模块的babel-node
命令,提供一个支持ES6
的REPL
环境。它支持Node
的REPL
环境的所有功能,而且可以直接运行ES6
代码。首先,安装这个模块。
然后,执行
babel-node
就进入REPL
环境。babel-node
命令可以直接运行ES6
脚本。将上面的代码放入脚本文件es6.js
,然后直接运行。第三小节 通过Traceur使用ES6
Google
公司的Traceur
编译器 ,可以将ES6
代码编译为ES5
代码,https//github.com/google/traceur-compiler
。3.1 Node 环境的用法
Traceur
的Node
用法如下(假定已安装traceur
模块)。3.2 命令行转换
作为命令行工具使用时,
Traceur
是一个Node
的模块,首先需要用npm
安装。安装成功后,就可以在命令行下使用
Traceur
了。Traceur
直接运行ES6
脚本文件,会在标准输出显示运行结果,以前面的calc.js
为例。如果要将
ES6
脚本转为ES5
保存,要采用下面的写法。上面代码的
--script
选项表示指定输入文件,--out
选项表示指定输出文件。为了防止有些特性编译不成功,最好加上
--experimental
选项。命令行下转换生成的文件,就可以直接放到浏览器中运行。
3.3 浏览器环境
Traceur
允许将ES6
代码直接插入网页。首先,必须在网页头部加载Traceur
库文件。上面代码中,一共有
4
个script
标签。第一个是加载Traceur
的库文件
,第二个和第三个是将这个库文件用于浏览器环境
,第四个则是加载用户脚本
,这个脚本里面可以使用ES6
代码。注意,第四个
script
标签的type
属性的值是module
,而不是text/javascript
。这是Traceur
编译器识别ES6
代码的标志,编译器会自动将所有type=module
的代码编译为ES5
,然后再交给浏览器执行。除了引用外部
ES6
脚本,也可以直接在网页中放置ES6
代码。正常情况下,上面代码会在控制台打印出
9
。如果想对
Traceur
的行为有精确控制,可以采用下面参数配置的写法。上面代码中,首先生成
Traceur
的全局对象window.System
,然后System.import
方法可以用来加载ES6
。加载的时候,需要传入一个配置对象metadata
,该对象的traceurOptions
属性可以配置支持 ES6 功能。如果设为experimental: true
,就表示除了ES6
以外,还支持一些实验性的新功能。3.4 在线转换
Traceur
也提供一个在线编译器,可以在线将ES6
代码转为ES5
代码。转换后的代码,可以直接作为ES5
代码插入网页运行。上面的例子转为
ES5
代码运行,就是下面这个样子。