文章目錄
  1. 1. 参数默认值
  2. 2. Rest参数
  3. 3. 参数解构
  4. 4. 扩展运算符
  5. 5. name属性
  6. 6. new.target,[[Call]]和[[Construct]]
  7. 7. 箭头函数
    1. 7.1. 语法
    2. 7.2. 词法的this绑定
    3. 7.3. 词法arguments绑定
    4. 7.4. 识别箭头函数

本文翻译自:https://leanpub.com/understandinges6/read#leanpub-auto-objectis,内容有删减,仅作为阅读笔记。

ES6中的函数增加了很多新特性,使得在JavaScript中编程不容易出错,且比以往任何时候还要强大。ES6的函数主要带来了以下扩展:参数的默认值、rest参数、解构参数、扩展(Spread)运算符、name属性、箭头函数等。下面就一一介绍。

参数默认值

JavaScript中,我们可以给函数传入任意多个参数,而不需要管它实际定义的形参个数。这允许我们定义的函数可以处理参数不同的情况,我们可以为没有指定的参数默认值。在ES5中我们可以经常看到下面这样的代码:

1
2
3
4
5
6
7
8
function makeRequest(url, timeout, callback) {

timeout = timeout || 2000;
callback = callback || function() {};

// the rest of the function

}

在这个例子中,timeoutcallback都是可选的,因为在没有传相应的参数时,它们有默认值2000function() {}。我们经常用这种方式来实现默认参数。

ES6中对函数进行了扩展,添加了参数默认值,在没有传入相应参数时,会使用你在定义函数时给定的默认值,而不是像ES5中那样对每个参数还要做一次判断。在ES6中我们可以这样:

1
2
3
4
5
function makeRequest(url, timeout = 2000, callback = function() {}) {

// the rest of the function

}

这样只有我们的第一个参数是期望传入的,而其他两个参数都有默认值,这使得函数更加简洁,我们不需要去判断是否某个参数没有被传入,就像timeout = timeout || 2000;这样。当makeRequest()调用时传入了所有的三个参数时,默认参数就不会被使用了。例如:

1
2
3
4
5
6
7
8
9
10
// uses default timeout and callback
makeRequest("/foo");

// uses default callback
makeRequest("/foo", 500);

// doesn't use defaults
makeRequest("/foo", 500, function(body) {
doSomething(body);
});

任何参数有默认值时我们认为它是可选的,而那些没有默认值的参数我们认为是必须的参数。

Rest参数

上面也说到JavaScript中是可以传入任意多个参数,有时候我们没有必要指定所有的参数。在以前我们可以通过arguments函数的所有参数,虽然这在大多数情况下可以工作的很好,但是还是会有一点小累赘。例如:

1
2
3
4
5
6
7
8
9
10
11
12
function sum(first) {
let result = first,
i = 1,
len = arguments.length;

while (i < len) {
result += arguments[i];
i++;
}

return result;
}

上面这个函数将所有传入的参数相加,例如我们可以sum(1)或者sum(1, 2, 3, 4)都是可以的。但这个函数我们有几件事情要注意。第一、函数不能明显的看出可以处理一个以上的参数。第二、因为我们指定了一个first参数,那我们就必须从arguments的索引1开始,而不是索引0。当然记住正确的索引并不是很困难,但是还是需要我们关注的一件事。ES6为我们提供了rest参数来解决这个问题。

rest参数由三个.加上参数名字来表示,那个参数名字成为一个数组包含了参数的其余部分。例如,sum()函数可以使用rest参数重写:

1
2
3
4
5
6
7
8
9
10
11
12
function sum(first, ...numbers) {
let result = first,
i = 0,
len = numbers.length;

while (i < len) {
result += numbers[i];
i++;
}

return result;
}

在这个版本的函数中,我们可以看到numbers包含了除first参数之外的剩余参数。这意味着我们可以从0开始遍历numbers,而不需要有任何顾虑。我们也可以很容易的看出函数可以处理任意多个参数。

对于rest参数有一个限制就是,在rest参数后不能再跟其他函数,否则会语法错误。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Syntax error: Can't have a named parameter after rest parameters
function sum(first, ...numbers, last) {
let result = first,
i = 0,
len = numbers.length;

while (i < len) {
result += numbers[i];
i++;
}

return result;
}

参数解构

在前面我们学过变量解构,解构同样也可以使用在函数的参数中。

通常我们会使用一个options对象作为一个参数来代替传入多个参数,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function setCookie(name, value, options) {

options = options || {};

var secure = options.secure,
path = options.path,
domain = options.domain,
expires = options.expires;

// ...
}

setCookie("type", "js", {
secure: true,
expires: 60000
});

除了namevalue是必须的之外,其他数据没有顺序优先级,这里我们使用了一个options对象来代替,而不是额外的命名参数。这种方式是OK的,但是给我们的函数带来了不透明性。

使用参数解构,之前的函数可以被重写为:

1
2
3
4
5
6
7
8
9
function setCookie(name, value, { secure, path, domain, expires }) {

// ...
}

setCookie("type", "js", {
secure: true,
expires: 60000
});

这个例子和前面的不同之处在于使用了参数解构取出了必要的数据。这样使得更清楚的知道需要什么样的参数。参数解构和变量解构一样,如果没有传入相应数据时,其值为undefined

有一点需要注意的是,当我们在上面的函数中没有传入第三个参数时,就像这样:

1
2
// Error!
setCookie("type", "js");

这个代码是会报错的,在内部执行时其实是像下面这样:

1
2
3
4
5
6
function setCookie(name, value, options) {

var { secure, path, domain, expires } = options;

// ...
}

因为变量解构时,当右边的表达式为nullundefined时是会报错的。不过我们可以通过传入一个默认的空对象来解决这个问题:

1
2
3
4
function setCookie(name, value, { secure, path, domain, expires } = {}) {

// ...
}

扩展运算符

扩展运算符和rest参数正好相反,rest参数允许我们将多个独立的参数合并成一个数组,而扩展运算符允许我们将一个数组分割,每个元素作为独立参数传入到函数中。我们可以考虑Math.max()方法,我们可以传入任意多个参数,然后返回最大的那个参数值。例如:

1
2
3
4
let value1 = 25,
value2 = 50;

console.log(Math.max(value1, value2)); // 50

当只有几个参数时我们很好处理,但是当我们的值存在一个数组中时我们应该怎么办呢?在ES5中我们可以通过apply来操作:

1
2
3
let values = [25, 50, 75, 100]

console.log(Math.max.apply(Math, values)); // 100

ES6的扩展运算符使这种情况变得更加简单。你可以通过在数组名前面加上...传入函数中,就像rest参数那样。例如:

1
2
3
4
5
let values = [25, 50, 75, 100]

// equivalent to
// console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values)); // 100

你也可以混合使用扩展运算符和其他参数。例如:

1
2
3
let values = [-25, -50, -75, -100]

console.log(Math.max(...values, 0)); // 0

name属性

ES6为所有函数添加了一个name属性,在ES6的程序中所有的函数的name属性都确保有一个合适的值。例如:

1
2
3
4
5
6
7
8
9
10
function doSomething() {
// ...
}

var doAnotherThing = function() {
// ...
};

console.log(doSomething.name); // "doSomething"
console.log(doAnotherThing.name); // "doAnotherThing"

在这段代码中,doSomething()函数的name属性值为doSomething,因为他是一个函数声明。在匿名函数表达式中,doAnotherThing()name属性值为doAnotherThing

下面看个更详细的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var doSomething = function doSomethingElse() {
// ...
};

var person = {
get firstName() {
return "Nicholas"
},
sayName: function() {
console.log(this.name);
}
}

console.log(doSomething.name); // "doSomethingElse"
console.log(person.sayName.name); // "sayName"
console.log(person.firstName.name); // "get firstName"

对于函数名还有几种特殊情况。对于使用bind()创建的函数会在它们的函数名前加上bound前缀。对于使用Function构造函数创建的函数它的名字为anonymous。例如:

1
2
3
4
5
6
7
var doSomething = function() {
// ...
};

console.log(doSomething.bind().name); // "bound doSomething"

console.log((new Function()).name); // "anonymous"

new.target[[Call]][[Construct]]

ES5甚至更早,函数服务于双重目的,通过new来调用或没有new。当使用new时,函数内的this值是返回的新对象。例如:

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); // "[Object object]"
console.log(notAPerson); // "undefined"

当没有使用new调用Person()时,返回undefined。这里很明显的是这段代码的意图是使用Personnew来创建一个对象。在ES6中,在函数双重角色的困惑上做了一些改变。

第一、规范定义了两个不同的仅在内部使用的方法,每个函数都有:[[Call]][[Construct]]。当一个函数没有通过new来调用时,[[Call]]方法会被执行。当一个函数通过new来调用时,[[Construct]]被调用。[[Construct]]方法有责任创建一个新的对象。被称之为new target,然后执行函数体,this的值被设置为new target。有一个[[Construct]]方法的函数被称之为构造函数

要注意的是不是所有的函数都有[[Construct]],也不是所有的函数能通过new来调用。箭头函数,将在后面介绍到,没有[[Construct]]方法。

ES5中,最流行的方式来决定一个函数能否通过new来调用是使用instanceof操作符。例如:

1
2
3
4
5
6
7
8
9
10
function Person(name) {
if (this instanceof Person) {
this.name = name; // using new
} else {
throw new Error("You must use new with Person.")
}
}

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

在这里,this的值会被检查是否为构造函数的实例,如果是的话,它继续正常执行。如果不是,就会抛出一个异常。这个能工作是因为[[Construct]]方法创建了Person的一个新实例,并将它赋给了this。不幸的是,这种方式不是完全可信的,this的值可以不通过new的方式也可以为Person的实例,例如:

1
2
3
4
5
6
7
8
9
10
function Person(name) {
if (this instanceof Person) {
this.name = name; // using new
} else {
throw new Error("You must use new with Person.")
}
}

var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // works!

我们这里通过Person.call(),并且传递person对象作为第一个参数,这就使得无法区分this是通过new创建的还是其他方式。

为了解决这个问题,ES6引入了new.target元属性。当一个函数的[[Construct]]被调用,new.target会成为新创建对象的实例,这个值也会在函数内成为this的值。如果[[Call]]被执行,new.targetundefined。这意味着我们现在可以通过检查new.target是否被定义安全的检查函数是否是通过new的方式被调用。例如:

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

var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // error!

箭头函数

ES6中新添加了一个箭头函数,就如它的名字,函数通过一种新的语法,使用箭头=>来定义。然而,箭头函数在有些方面和传统的JavaScript函数是不同的:

  • 词法的this绑定:在该函数内的this是通过定义箭头函数的地方决定,而不是使用它的地方决定。
  • 不能当作构造函数:箭头函数没有[[Construct]]方法,因此它不能作为构造函数。当箭头函数与new一起使用时将会抛出异常。
  • 不可以改变this值:在函数内的this的值是不能改变的,它在函数的整个生命周期中保持相同的值。
  • 没有arguments对象:你不能通过arguments对象来获取参数,你必须使用命名参数或者rest参数等。

其中有一些原因来表明这些不同为什么存在。首先,this的绑定在JavaScript是一个常见的错误根源。在一个函数内对this值的跟踪是非常容易丢失的,这可能会导致意想不到的后果。第二,通过限制箭头函数在执行代码时只有单一的this值,JavaScript引擎能更好的优化操作。

语法

箭头函数的语法有多种风格,这取决于你试图完成什么。所有的变种(箭头函数的多种风格)都是以函数参数开始,跟着箭头,跟着函数体。参数和函数体取决于使用可以采取不同的形式。例如,下面的箭头函数接受一个单一参数和简单的返回它:

1
2
3
4
5
6
7
var reflect = value => value;

// effectively equivalent to:

var reflect = function(value) {
return value;
};

当箭头函数只有一个参数时,我们只需直接使用这个参数而不需要其他的语法。然后箭头函数的右边会被计算和返回。即使我们这没有一个明确的return声明,箭头函数会返回传入的第一个参数。

如果你传入的参数个数大于1时,你就需要圆括号将参数用括号扩起来啦。例如:

1
2
3
4
5
6
7
var sum = (num1, num2) => num1 + num2;

// effectively equivalent to:

var sum = function(num1, num2) {
return num1 + num2;
};

sum()函数将两个参数相加并返回结果。不同之处在于我们的参数放在圆括号内,且用,分隔。

需要注意的是,当我们的箭头函数没有参数时,我们必须包含一个空圆括号。例如:

1
2
3
4
5
6
7
var getName = () => "Nicholas";

// effectively equivalent to:

var getName = function() {
return "Nicholas";
};

当你需要提供一个更传统的函数体时,可能包含多个表达式,那么我们可以将这些语句放在花括号{}内,并且定义一个明确的返回值。例如:

1
2
3
4
5
6
7
8
9
var sum = (num1, num2) => {
return num1 + num2;
};

// effectively equivalent to:

var sum = function(num1, num2) {
return num1 + num2;
};

当你需要创建一个什么也不做的函数时,我们需要包括花括号,例如:

1
2
3
4
5
var doNothing = () => {};

// effectively equivalent to:

var doNothing = function() {};

还有一点需要注意的是,因为花括号被用于包含函数体,当我们需要从箭头函数返回一个对象直接量时,我们需要将直接量放在圆括号内。例如:

1
2
3
4
5
6
7
8
9
10
11
var getTempItem = id => ({ id: id, name: "Temp" });

// effectively equivalent to:

var getTempItem = function(id) {

return {
id: id,
name: "Temp"
};
};

词法的this绑定

JavaScript中最常见出错的地方就是this在函数内的绑定。因为this的值可以根据调用它的上下文在单个函数内改变,这就可能错误的使用影响某个对象,但你的意图是另外一个对象。考虑以下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var PageHandler = {

id: "123456",

init: function() {
document.addEventListener("click", function(event) {
this.doSomething(event.type); // error
}, false);
},

doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};

在这段代码中,我们知道this.doSomething中的this对象其实是指向document的,但我们的本意其实是PageHandler对象。如果你试图运行代码,会得到一个错误。你可能会使用bindvar me = 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
// bind way
var PageHandler = {

id: "123456",

init: function() {
document.addEventListener("click", (function(event) {
this.doSomething(event.type); // no error
}).bind(this), false);
},

doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};

// var me = this;
var PageHandler = {

id: "123456",

init: function() {
var me = this;
document.addEventListener("click", function(event) {
me.doSomething(event.type); // no error
}, false);
},

doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};

现在代码如你预期的那样执行了,但是我们总会觉得有些奇怪。通过调用bind(this),你实际上是创建了一个新的函数,新函数的this被绑定到了PageHandler

但是在箭头函数内,它具有隐式this绑定,这意味着箭头函数内的this值总是与定义箭头函数的作用域的this具有相同的值。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
var PageHandler = {

id: "123456",

init: function() {
document.addEventListener("click",
event => this.doSomething(event.type), false);
},

doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};

现在this的值和init()内的this值是一样的,它就能用bind的方式实现一样的功能了。

箭头函数被设计为“一次性“函数,所以它不能用于定义新的类型。它和普通的函数不同,它没有prototype属性。如果你尝试在箭头函数上使用new操作符,就会报错。例如:

1
2
var MyType = () => {},
object = new MyType(); // error - you can't use arrow functions with 'new'

因为this的值是静态绑定到箭头函数上,你不能通过apply()call()bind()的方式来改变this的值。

词法arguments绑定

尽管箭头函数自身没有arguments对象,但是它能获取到包含它的函数的arguments对象。例如:

1
2
3
4
5
6
7
function createArrowFunctionReturningFirstArg() {
return () => arguments[0];
}

var arrowFunction = createArrowFunctionReturningFirstArg(5);

console.log(arrowFunction());

识别箭头函数

尽管语法不同,箭头函数同样也是函数,它也可以被识别:

1
2
3
4
var comparator = (a, b) => a - b;

console.log(typeof comparator); // "function"
console.log(comparator instanceof Function); // true

至此,就是本文的全部内容啦。

文章目錄
  1. 1. 参数默认值
  2. 2. Rest参数
  3. 3. 参数解构
  4. 4. 扩展运算符
  5. 5. name属性
  6. 6. new.target,[[Call]]和[[Construct]]
  7. 7. 箭头函数
    1. 7.1. 语法
    2. 7.2. 词法的this绑定
    3. 7.3. 词法arguments绑定
    4. 7.4. 识别箭头函数