ES6之函数

函数形参默认值

ES5默认形参

ES5中模拟默认形参

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

但是这种方法也有缺陷,如果第二个值我们传0,会被视为一个假值,函数timeout最后取值却为2000。

更好的做法是:

1
2
3
4
function makeRequest(url, timeout, callback) {
timeout = typeof timeout !== undefined ? timeout : 2000;
callback = typeof callback !== undefined ? callback : function() {};
}

ES6默认形参

ES6做法:

1
2
3
function makeRequest(url, timeout=2000, callback=function(){}) {
// do something
}

注意: 当timeout传值为undefined时,最终取值为默认值2000;

1
2
3
4
function makeRequest(url, timeout = 2000, callback) {
console.log(timeout)
}
makeRequest('/foo', undefined); // 2000

对arguments影响

ES5非严格模式下,形参值被改变,arguments对应的也跟着改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
function mixArgs(first, second) {
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a", "b");
// true
// true
// true
// true

严格模式下,无论形参如何被赋值,arguments不再跟着改变;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function mixArgs(first, second) {
'use strict';
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a", "b");
// true
// true
// false
// false

ES6中,如果使用了默认参数,则无论是否显式定义了严格模式,arguments行为与ES5严格模式下保持一致;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function mixArgs(first, second = "b") {
console.log(arguments.length);
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d"
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a");
// 1
// true
// false
// false
// false

默认参数表达式

默认参数也可以使用表达式;

默认参数表达式求值条件:当调用函数不传入默认参数对应位置的值时才会调用;

1
2
3
4
5
6
7
8
9
10
let value = 5;
function getValue() {
return value++;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
console.log(add(1)); // 7

默认参数临时死区(TDZ)

默认参数也类似let有默认死区,后面的参数不能用在之前参数的默认值上:

1
2
3
4
5
function add(first = second, second) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // Uncaught ReferenceError: second is not defined

无命名参数

由于之前当有多个无命名参数时,我们需要利用arguments参数来处理,有一个问题是需要记录从哪个索引开始,比较麻烦;

1
2
3
4
5
6
7
8
function pick(object) {
let result = Object.create(null);
// start at the second parameter
for (let i = 2, len = arguments.length; i < len; i++) {
result[arguments[i]] = object[arguments[i]];
}
return result;
}

ES6做法:

1
2
3
4
5
6
7
8
function pick(object, ...keys) {
let result = Object.create(null);
// start at the second parameter
for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]];
}
return result;
}

不定参数使用限制:最多只能一个不定参数,而且一定要放在所有参数的最后;

1
2
3
4
5
6
7
8
function pick(object, ...keys, last) {
let result = Object.create(null);
for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]];
}
return result;
}
pick() // error

增强的Function构造函数

Function构造函数也可以使用默认参数和无命名参数:

1
var add = new Function("first", "second = first", "...rest", "return first + second");

展开运算符

1
2
let values = [-25, -50, -75, -100];
console.log(Math.max(...values, 0));

name属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var doSomething = function doSomethingElse() {
// empty
};
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
console.log(doSomething.bind().name); // bound doSomethingElse
console.log((new Function()).name); // anonymous

注意:

  1. 命名函数(doSomethingElse)并且赋值了一个变量(doSomething),取命名的名字doSomethingElse;
  2. get属性的函数前面会有一个get; // chrome是返回undefined,需要确认是不是实现不一致;
  3. 利用bind生成的新函数前面有一个bound
  4. Function构造函数生成的函数为anonymous

明确函数的多重用途

JS函数有两个不同的内部方法: [[Call]]和[[Construct]]。当通过new关键字调用函数时,执行的是[[Construct]],它负责创建一个通常被称为实例的新对象,然后再执行函数体,将this绑定到新实例上;如果不通过new关键字调用函数,则执行[[Call]]函数,从而直接执行代码中的函数体。具有[[Construct]]方法的函数被统称为构造函数。

注意:不是所有的函数都有[[Construct]]方法,例如箭头函数就没有[[Construct]]方法,所以箭头函数不能使用new关键字调用;

之前ES5判断一个函数是不是通过’new’调用会判断this的原型, 但是这也有问题,函数执行没有用new但使用call来改变this同样可以不报错:

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");
var notAPerson = Person.call(person, "Michael"); // works

元属性new.target

为了解决判断函数是否通过new关键字调用的问题,ES6引入了new.target这个元属性;

元属性是指非对象的属性,其可以提供非对象目标的补充信息;当调用函数的[[Construct]]方法时,new.target被赋值为new操作符的目标,通常是新创建对象实例,也就是函数体内this的构造函数,如果调用[[Call]]方法,则new.target的值为undefined。则判断一个函数是否是通过new关键字调用的,可以这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(name) {
if (new.target === Person) {
this.name = name;
} else {
throw new Error("You must use new with Person.")
}
}
function AnotherPerson(name) {
Person.call(this, name);
}
var person = new Person("Nicholas");
var person = Person("Zakas"); //Uncaught Error: You must use new with Person.
var anotherPerson = new AnotherPerson("Nicholas");// Uncaught Error: You must use new with Person.

在函数体外使用new.target是一个语法错误

块级函数

ES5严格模式下声明块级函数会抛出错误;ES6中,会将块级函数视作一个块级声明,从而在代码块内可以访问和调用;但是块级函数会被提升至顶部;

1
2
3
4
5
6
7
8
if (true) {
console.log(typeof doSomething);
function doSomething() {
// empty
}
doSomething();
}
console.log(typeof doSomething); // function

如果要真正的定义块级函数,可以使用let表达式,这样函数表达式就不会提升了;并且有临时死区的情况,如:

1
2
3
4
5
6
7
if (true) {
console.log(typeof doSomething); // error
let doSomething = function () {
// empty
}
doSomething();
}

箭头函数

  1. 无this super arguments和new.target绑定

这些值由外围最近一层非箭头函数决定;

1
2
3
4
5
6
7
8
9
10
let PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click",
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};

  1. 不能通过new关键字调用

正如之前提到过的箭头函数没有[[Constructor]]方法,不能通过new关键字调用,;

1
2
var MyType = () => {},
object = new MyType(); // MyType is not a constructor
  1. 没有原型
1
2
3
4
var MyType = () => {};
var test = function() {};
console.log(MyType.prototype); // undefined
console.log(test.prototype.toString()); // [object Object]
  1. 不可以改变this的绑定
1
2
3
4
var MyType = () => {console.log(this)};
MyType.bind({name: 'hi'})(); // Window
var test = function() {console.log(this)};
test.bind({name: 'hi'})(); // {name: 'hi'}
  1. 不支持arguments对象
    绑定的arguments是外围非箭头函数的arguments
1
2
3
4
5
function createArrowFunctionReturningFirstArg() {
return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction());
  1. 不支持重复的命名参数

传统函数,只有严格模式不能使用重名的参数;

1
2
3
function test(name, name){};
'use strict';
function test(name, name){}; //Duplicate parameter name not allowed in this context

箭头函数中,不支持重复的命名参数

1
var test = (name, name) => {console.log(this)} // Duplicate parameter name not allowed in this context

尾调用优化

ES6缩减了严格模式下尾调用栈的大小(非严格模式下不影响),如果满足以下条件,尾调用不再创建新的栈帧,而是清楚并重用当前栈帧:

  • 尾调用不访问当前栈帧的变量;(函数不是一个闭包)
  • 在函数内部,尾调用是最后一条语句;
  • 尾调用的结果作为函数值返回;

满足条件的尾调用:

1
2
3
4
5
"use strict";
function doSomething() {
// optimized
return doSomethingElse();
}

不满足第一个条件

1
2
3
4
5
6
7
"use strict";
function doSomething() {
var num = 1,
func = () => num;
// not optimized - function is a closure
return func();
}

不满足第二个条件

1
2
3
4
5
6
"use strict";
function doSomething() {
// not optimized - call isn't in tail position
var result = doSomethingElse();
return result;
}

不满足第三个条件

1
2
3
4
5
"use strict";
function doSomething() {
// not optimized - must add after returning
return 1 + doSomethingElse();
}