本文是基于 ECMAScript 6 入门 的学习笔记。
只是按照本人理解梳理内容,更加详细的相关内容请移步 ECMAScript 6 入门 。javascript
es6.ruanyifeng.com/#docs/introhtml
Babel 是一个普遍使用的 ES6 转码器,能够将 ES6 代码转为 ES5 代码,从而在现有环境执行。这意味着,你能够用 ES6 的方式编写程序,又不用担忧现有环境是否支持。java
在项目目录下,安装Babelnode
$ npm install --save-dev @babel/core
复制代码
Babel 的配置文件是.babelrc
,存放在项目的根目录下。使用 Babel 的第一步,就是配置这个文件。git
(具体配置看原文)程序员
注意,如下全部 Babel 工具和模块的使用,都必须先写好.babelrc
。es6
Babel 提供命令行工具@babel/cli
,用于命令行转码。 它的安装命令以下:github
$ npm install --save-dev @babel/cli
复制代码
基本用法以下:npm
# 转码结果输出到标准输出
$ npx babel example.js
# 转码结果写入一个文件
# --out-file 或 -o 参数指定输出文件
$ npx babel example.js --out-file compiled.js
# 或者
$ npx babel example.js -o compiled.js
# 整个目录转码
# --out-dir 或 -d 参数指定输出目录
$ npx babel src --out-dir lib
# 或者
$ npx babel src -d lib
# -s 参数生成source map文件
$ npx babel src -d lib -s
复制代码
@babel/node
模块的babel-node
命令,提供一个支持 ES6 的 REPL 环境。它支持 Node 的 REPL 环境的全部功能,并且能够直接运行 ES6 代码。编程
@babel/register
模块改写require
命令,为它加上一个钩子。此后,每当使用require
加载.js
、.jsx
、.es
和`.es6后缀名的文件,就会先用 Babel 进行转码。
使用时,必须首先加载@babel/register
。
// index.js
require('@babel/register');
require('./es6.js');
复制代码
须要注意的是,@babel/register
只会对require
命令加载的文件转码,而不会对当前文件转码。另外,因为它是实时转码,因此只适合在开发环境使用。
若是某些代码须要调用 Babel 的 API 进行转码,就要使用@babel/core
模块。
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
文件。
Babel 也能够用于浏览器环境,使用@babel/standalone
模块提供的浏览器版本,将其插入网页。
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel">
// Your ES6 code
</script>
复制代码
注意,网页实时将 ES6 代码转为 ES5,对性能会有影响。生产环境须要加载已经转码完成的脚本。
Babel 提供一个REPL 在线编译器,能够在线将 ES6 代码转为 ES5 代码。转换后的代码,能够直接做为 ES5 代码插入网页运行。
Google 公司的Traceur转码器,也能够将 ES6 代码转为 ES5 代码。
Traceur 容许将 ES6 代码直接插入网页。首先,必须在网页头部加载 Traceur 库文件。
// 第一个是加载 Traceur 的库文件
<script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
// 第二个和第三个是将这个库文件用于浏览器环境
<script src="https://google.github.io/traceur-compiler/bin/BrowserSystem.js"></script>
<script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
// 第四个则是加载用户脚本,这个脚本里面可使用 ES6 代码。
<script type="module">
import './Greeter.js';
</script>
复制代码
注意,第四个script
标签的type
属性的值是module
,而不是text/javascript
。这是 Traceur 编译器识别 ES6 代码的标志,编译器会自动将全部type=module
的代码编译为 ES5,而后再交给浏览器执行.
除了引用外部 ES6 脚本,也能够直接在网页中放置 ES6 代码。 若是想对 Traceur 的行为有精确控制,能够采用下面参数配置的写法。(看原文)
Traceur 也提供一个在线编译器,能够在线将 ES6 代码转为 ES5 代码。转换后的代码,能够直接做为 ES5 代码插入网页运行。
做为命令行工具使用时,Traceur 是一个 Node 的模块,首先须要用 npm 安装。
$ npm install -g traceur
复制代码
Traceur 直接运行 ES6 脚本文件,会在标准输出显示运行结果。如下面的calc.js
为例。
<script type="module">
class Calc {
constructor() {
console.log('Calc constructor');
}
add(a, b) {
return a + b;
}
}
var c = new Calc();
console.log(c.add(4,5));
</script>
复制代码
$ traceur calc.js
复制代码
若是要将 ES6 脚本转为 ES5 保存,要采用下面的写法。
$ traceur --script calc.es6.js --out calc.es5.js --experimental
复制代码
--script
选项表示指定输入文件,--out
选项表示指定输出文件。 为了防止有些特性编译不成功,最好加上--experimental
选项。
相关解释看原文
let
命令声明的变量,只在let
命令所在的代码块内有效。{
let a = 10;
var b = 1;
}
a // ReferenceError: a is not defined.
b // 1
复制代码
{
console.log(i)
let i=1
}
Uncaught ReferenceError: i is not defined
//在一个块做用域里,只要使用let/const命令声明i,在let/const命令前使用i都会报错,
//这就是暂时性死区。
复制代码
var
命令会发生“变量提高”现象,即变量能够在声明以前使用,值为undefined
。这种现象多多少少是有些奇怪的,按照通常的逻辑,变量应该在声明语句以后才可使用。
为了纠正这种现象,let
命令改变了语法行为,它所声明的变量必定要在声明后使用,不然报错。
// var 的状况
console.log(foo); // 输出undefined
var foo = 2;
以上代码实际为:
var foo
console.log(foo); // 因此此时foo为undefined
foo = 2;
// let 的状况
// 由于 let 没有变量提高,因此 console 语句时 bar 是不存在的
console.log(bar); // 报错ReferenceError:bar is not defined
let bar = 2;
复制代码
只要块级做用域内存在let
命令,它所声明的变量就“绑定”(binding)这个区域,再也不受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
复制代码
上面代码中,存在全局变量tmp
,可是块级做用域内let
又声明了一个局部变量tmp
,致使后者绑定这个块级做用域,因此在let
声明变量前,对tmp
赋值会报错。
ES6 明确规定,若是区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就造成了封闭做用域。凡是在声明以前就使用这些变量,就会报错。
总之,在代码块内,使用let
命令声明变量以前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
复制代码
“暂时性死区”也意味着typeof
再也不是一个百分之百安全的操做。
// 1. 直接对一个没有声明的变量使用typeof 获得的是 undefined
typeof undeclared_variable // "undefined"
// 2. 可是若是这个变量是let声明的,就会报错。
typeof x; // ReferenceError
let x;
复制代码
这样的设计是为了让你们养成良好的编程习惯,变量必定要在声明以后使用,不然就报错。
有些死区比较隐蔽:
// x的默认值是y,可是此时y未声明,x=y 就是死区,因此就报错了。
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
复制代码
使用let
声明变量时,只要变量在尚未声明完成前使用,就会报错:
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
复制代码
总之,暂时性死区的本质就是,只要一进入当前做用域,所要使用的变量就已经存在了,可是不可获取,只有等到声明变量的那一行代码出现,才能够获取和使用该变量。
let
不容许在相同做用域内,重复声明同一个变量。
// 报错
function func() {
let a = 10;
var a = 1;
}
// 报错
function func() {
let a = 10;
let a = 1;
}
复制代码
所以,不能在函数内部从新声明参数。
function func(arg) {
let arg;
}
func() // 报错
function func(arg) {
{
let arg;
}
}
func() // 不报错
复制代码
var tmp = new Date();
function f() {
console.log(tmp);
if (false) {
var tmp = 'hello world';
}
}
f(); // undefined
此处 console 语句是想外部使用外部的 tmp ,if内部使用内部的 tmp 。
可是 if 内部的 tmp 泄露到 if 外 ,致使了咱们预期外的结果。
复制代码
第二种场景,用来计数的循环变量泄露为全局变量。
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
此处 for 结束后,i就应该消失,可是却还能打印出i,
这会干扰的别的也使用 i 做为全局变量的地方。
复制代码
let
实际上为 JavaScript 新增了块级做用域。function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
此处 if 内 let声明的n 就不会干扰到 if 外的 n 。
若是使用的都是var 则最后打印出的是 10 。
复制代码
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级做用域写法
{
let tmp = ...;
...
}
复制代码
- 容许在块级做用域内声明函数。
- 函数声明相似于var,即会提高到全局做用域或函数做用域的头部。
- 同时,函数声明还会提高到所在的块级做用域的头部。
上面三条规则只对 ES6 的浏览器实现有效,其余环境的实现不用遵照,
仍是将块级做用域的函数声明看成let处理。
复制代码
综上,应该避免在块级做用域内声明函数。若是确实须要,也应该写成函数表达式,而不是函数声明语句。
// 块级做用域内部,优先使用函数表达式
{
let a = 'secret';
let f = function () {
return a;
};
}
复制代码
ES6 的块级做用域必须有大括号,若是没有大括号,JavaScript 引擎就认为不存在块级做用域。
// 第一种写法,报错
if (true) let x = 1;
// 第二种写法,不报错
if (true) {
let x = 1;
}
复制代码
函数声明也是如此,严格模式下,函数只能声明在当前做用域的顶层。
const
声明一个只读的常量。一旦声明,常量的值就不能改变。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
复制代码
const
声明的变量不得改变值,这意味着,const
一旦声明变量,就必须当即初始化,不能留到之后赋值。
const foo;
// SyntaxError: Missing initializer in const declaration
复制代码
const
与 let
同样, 有块级做用域,暂时性死区,声明的常量不提高,也不能重复声明。
ES5 只有两种声明变量的方法:var
命令和function
命令。ES6 除了添加let
和const
命令,另外两种声明变量的方法:import
命令和class
命令。因此,ES6 一共有 6 种声明变量的方法。
顶层对象,在浏览器环境指的是 window 对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。
顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,
ES6 为了改变这一点,且保证兼容性,规定 var
、function
声明的全局变量依旧是顶层对象的属性;而let
、const
、class
声明的全局变量不属于顶层对象的属性。
var a = 1;
// 若是在 Node 的 REPL 环境,能够写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
复制代码
JavaScript 语言存在一个顶层对象,它提供全局环境(即全局做用域),全部代码都是在这个环境中运行。可是,顶层对象在各类实现里面是不统一的。
同一段代码为了可以在各类环境,都能取到顶层对象,如今通常是使用this
变量,可是有局限性。
this
会返回顶层对象。可是,Node 模块和 ES6 模块中,this
返回的是当前模块。this
,若是函数不是做为对象的方法运行,而是单纯做为函数运行,this
会指向顶层对象。可是,严格模式下,这时this
会返回undefined
。new Function('return this')()
,老是会返回全局对象。可是,若是浏览器用了 CSP(Content Security Policy,内容安全策略),那么eval
、new Function
这些方法均可能没法使用。如今有一个提案,在语言标准的层面,引入globalThis
做为顶层对象。也就是说,任何环境下,globalThis
都是存在的,均可以从它拿到顶层对象,指向全局环境下的this
。
垫片库global-this
模拟了这个提案,能够在全部环境拿到globalThis
。