理解 JavaScript 编程(ECMAScript 6)(一)

一、Block Binding

在大多数基于 C 的编程语言中,变量通常会在声明时创建。而对于 JavaScript 语言,变量创建的时间点则取决于具体的声明方式。
JavaScript 中经典的使用 var 声明变量的方式容易引起困惑,因此 ECMAScript 6 中引入了块级别的变量绑定(block-level binding)。

var 关键字

var 关键字对于变量的声明,会默认该声明位于函数顶部(位于函数外部时为全局作用域),而不去管声明语句实际出现的位置。称为 hoisting

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function getValue(condition) {
if (condition) {
var value = "blue"
console.log("condition is true and value is " + value)
} else {
console.log("condition is false and value is " + value)
}
console.log("outside if, value is " + value)
}

getValue(true)
// condition is true and value is blue
// outside if, value is blue
getValue(false)
// condition is false and value is undefined
// outside if, value is undefined

习惯上会认为,上述代码中只有 condition 为 True 时变量 value 才会被创建;实际上 value 变量存在于函数的各个部分,只是在 condition 为 False 时未被初始化(undefined)。

上面的代码会被 JavaScript 引擎视作如下形式:

1
2
3
4
5
6
7
8
9
10
function getValue(condition) {
var value;
if (condition) {
value = "blue";
console.log("condition is true and value is " + value)
} else {
console.log("condition is false and value is " + value)
}
console.log("outside if, value is " + value)
}

块级声明和 let 语句

由块级声明创建的变量无法被该代码块以外的部分访问。

块作用域(Block scopes)一般创建于以下位置:

  • 函数内部
  • 代码块内部(被大括号 {} 包裹的部分)

块作用域符合大部分基于 C 的编程语言的工作方式。

let 关键字会将变量的作用域限制在当前代码块内部。

1
2
3
4
5
6
7
8
9
10
11
12
13
function getValue(condition) {
if (condition) {
let value = "blue"
console.log("condition is true and value is " + value)
} else {
console.log("condition is false and value is " + value)
}
console.log("outside if, value is " + value)
}

getValue(true)
// condition is true and value is blue
// ReferenceError: value is not defined

No Redeclaration

如果同一作用域内已有相同名称的变量被声明,则 let 语句会报错。

1
2
3
var count = 30
let count = 40
// SyntaxError: Identifier 'count' has already been declared

但是在不同作用域中,类似的情况则不会报错:

1
2
3
4
var count = 30
if (true) {
let count = 40
}

const 关键字用于声明常量,常量的值一旦确定后即不可再变更,因此在声明的同时必须赋值以完成初始化。

1
2
3
const maxItems = 30
const name;
// SyntaxError: Missing initializer in const declaration

需要注意的是,常量的“不可变”仅针对变量与值的绑定关系,而并不限制值本身的改动。即使用 const 声明某个对象,则对象本身的改动不被禁止。

1
2
3
4
5
6
7
8
9
10
const person = {
name: "Nicholas"
}
person.name = "Greg"
person.name
// 'Greg'
person = {
name: "Grep"
}
// TypeError: Assignment to constant variable.

循环中的块级绑定

var:

1
2
3
4
5
for (var i = 0; i < 10; i++) {
// do nothing
}
console.log(i)
// 10

let:

1
2
3
4
5
for (let i = 0; i < 10; i++) {
// do nothing
}
console.log(i)
// ReferenceError: i is not defined

var 声明语句的特性(loop 变量可以从 loop 外部访问)使得在循环中创建函数的行为会产生问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var funcs = []

for (var i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i)
})
}

funcs.forEach(function(func) {
func()
})
// 10
// 10
// 10
// 10
// 10
// 10
// 10
// 10
// 10
// 10

解决的办法是使用如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var funcs = []

for (var i = 0; i < 10; i++) {
funcs.push((function(value) {
return function() {
console.log(value)
}
}(i)))
}

funcs.forEach(function(func) {
func()
})
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9

有了块级声明以后,上述需求可以被简单地实现(只需要将第一段代码中的 var 关键字改为 let 即可):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var funcs = []

for (let i = 0; i < 10; i++) {
funcs.push(function() {
console.log(i)
})
}

funcs.forEach(function(func) {
func()
})
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9

二、函数

参数带默认值的函数

在 ECMAScript 5 及以前版本的 JavaScript 中,通常使用如下模式创建带默认参数的函数:

1
2
3
4
5
function makeRequest(url, timeout, callback) {
timeout = timeout || 2000
callback = callback || function() {}
// the rest code
}

但上述 || (或)操作符的使用存在一定问题,如传递给 timeout 参数的值为 0 时,timeout || 2000 表达式的值为 2000 而不是 0,导致程序出现意想不到的结果。改进如下:

1
2
3
4
5
function makeRequest(url, timeout, callback) {
timeout = (typeof timeout !== "undefined") ? timeout : 2000
callback = (typeof callback !== "undefined") ? callback : function() {}
// the rest code
}

在 ECMAScript 6 中,为函数的参数提供默认值的方式则非常简单直观:

1
2
3
function makeRequest(url, timeout = 2000, callback = function() {}) {
// the rest code
}

表达式作为参数默认值

1
2
3
4
5
6
7
8
9
10
function getValue() {
return 5
}

function add(first, second = getValue()) {
return first + second
}

console.log(add(1, 1)) // 2
console.log(add(1)) // 6

甚至可以使用如下代码:

1
2
3
4
5
6
7
8
9
10
function getValue(value) {
return value + 5
}

function add(first, second = getValue(first)) {
return first + second
}

console.log(add(1, 1)) // 2
console.log(add(1)) // 7

匿名参数

ECMAScript 5 中的匿名参数(通过 arguments 对象获取所有参数,包含定义函数时未显式指定的参数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function pick(object) {
let result = Object.create(null)
for (let i = 1, len = arguments.length; i < len; i++) {
result[arguments[i]] = object[arguments[i]]
}
return result
}

let book = {
title: "Understanding ECMAScript 6",
author: "Nicholas C. Zakas",
year: 2016
}

let bookData = pick(book, "author", "year")

console.log(bookData.author) // "Nicholas C. Zakas"
console.log(bookData.year) // 2016

注意 for 循环是从 i=1 即第二个参数开始的。

Rest Parameters
上述 pick 函数可以利用 ECMAScript 6 支持的 Rest Parameters 特性重写为如下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function pick(object, ...keys) {
let result = Object.create(null)

for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]]
}
return result
}

let book = {
title: "Understanding ECMAScript 6",
author: "Nicholas C. Zakas",
year: 2016
}

let bookData = pick(book, "author", "year")

console.log(bookData.author) // "Nicholas C. Zakas"
console.log(bookData.year) // 2016

函数构造器
1
2
3
var add = new Function("first", "second", "return first + second")

console.log(add(1, 1)) // 2

ECMAScript 6 使得函数构造器可以支持默认参数和 rest parameters 等特性:

1
2
3
4
5
6
7
8
var add = new Function("first", "second = first", "return first + second")

console.log(add(1, 1)) // 2
console.log(add(1)) // 2

var pickFirst = new Function("...args", "return args[0]")

console.log(pickFirst(1, 2)) // 1

函数的两种调用方式
1
2
3
4
5
6
7
8
9
function Person(name) {
this.name = name
}

var person = new Person("Nicholas")
var notAPerson = Person("Nicholas")

console.log(person) // Person { name: 'Nicholas' }
console.log(notAPerson) // undefined

JavaScript 有两个针对函数的内部方法:[[Call]][[Construct]]

当函数不通过 new 关键字调用时,[[Call]] 方法执行,接着运行函数体中的代码;
当函数通过 new 关键字调用时,[[Construct]] 方法执行,创建一个新的对象实例并绑定给 this,之后继续执行函数体中的代码。

ECMAScript 6 中可以使用 new.target 判断当前函数是否由 new 调用:

1
2
3
4
5
6
7
8
9
10
function Person(name) {
if (typeof new.target !== "undefined") {
this.name = name
} else {
throw new Error("You must use new with Person.")
}
}

var person = new Person("Nicholas")
var notAPerson = Person("Michael") // error

Arrow Function

Arrow Function 是指使用 => 符号定义的函数。与传统的 JavaScript 函数相比,Arrow Function 主要有以下几个不同点:

  • 没有 this, super, arguments, new.target 的绑定。Arrow Function 中 this, super, arguments, new.target 的值由距离最近的非 Arrow Function 定义
  • 不能被 new 调用。Arrow Function 没有构造方法因此不能作为构造器使用
  • 没有 prototype。Arrow Function 的 prototype 属性不存在(函数本身不能被 new 调用,prototype 没有必要)
  • 函数中的 this 的值不能被修改

基本语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
let reflect = value => value

// equivalent to:
let reflect = function(value) {
return value
}


let sum = (num1, num2) => num1 + num2

// equivalent to:
let sum = function(num1, num2) {
return num1 + num2
}


let getName = () => "Nicholas"

// equivalent to:
let getName = function() {
return "Nicholas"
}


let doNothing = () => {}

// equivalent to:
let doNothing = function() {}


let getTempItem = id => ({ id: id, name: "Temp" })

// equivalent to:
let getTempItem = function(id) {
return {
id: id,
name: "Temp"
}
}

参考资料

Understanding ECMAScript 6