【读书笔记】【深入理解ES6】#3-函数
2017-11-23 16:52 佳佳的博客 阅读(207) 评论(0) 编辑 收藏 举报函数形参的默认值
ES6中的默认参数值
function makeRequest(url, timeout = 2000, callback = function() {}) {
}
可以为任意参数指定默认值,在已指定默认值的参数后可以继续声明无默认值参数。
function makeRequest(url, timeout = 2000, callback) {
}
这种情况下,之后当不为第二个参数传入值或者主动为第二个参数传入 undefined 时才会使用 timeout 的默认值
// 使用 timeout 的默认值
makeRequest("/foo", undefined, function(body) {
doSomething(body);
})
// 使用 timeout 的默认值
makeRequest("/foo");
// 不使用 timeout 的默认值
makeRequest("/foo", null, function(body) {
doSomething(body);
})
第三个调用需要注意。对于默认参数值,null是一个合法值。
关于 null 和 undefined 的区别请参照阮一峰的 undefined与null的区别。
默认参数值对 arguments 对象的影响
在ES5非严格模式下,函数命名参数的变化会体现在 arguments 对象中。
function mixArgs(first, second) {
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", "b");
// 2
// true
// true
// true
// true
然而在ES5严格模式下,取消了 arguments 对象这个令人感到困惑的行为。
命名参数与 arguments 对象分离开了。
function mixArgs(first, second) {
"use strict"; // 设置为严格模式
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", "b");
// 2
// true
// true
// false
// false
在ES6中,如果一个函数使用了默认参数值,则无论是否显示定义了严格模式,arguments 对象的行为都将是与ES5严格模式下保持一致。
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
mixArgs("a", "b");
// 2
// true
// true
// false
// false
默认参数表达式
默认参数值可以是非原始值传参。
function getValue() {
return 5;
}
function add(first, second = getValue()) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
上例中,second 的默认值为 getValue() 的返回值。
默认参数还可以使用先定义的参数作为后定义参数的默认值。
function add(first, second = first) {
return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 2
还可以将上面两个例子合起来修改成如下形式:
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
在引用参数默认值时,只允许引用前面参数的值,即先定义的参数不能访问后定义的参数。
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
因为second比first定义的晚,所以不能作为fist的默认值。
这里就是所谓默认参数的临时死区(TDZ)。
Note
默认参数有自己的作用域和临时死区,其与函数体的作用域是各自独立的,也就是说参数的默认值不可访问函数体内声明的变量。
处理无命名参数
JS的函数语法规定,无论函数已定义的命名参数有多少,都不限制调用时传入的实际参数的数量,调用时总是可以传入任意数量的参数。
ES5中的无命名参数
JS提供了 arguments 对象来检查函数的所有参数,从而不必定义每一个要用的参数。
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
上例中,pick() 函数返回一个给定对象的副本,包含原始对象的特定子集。
不定参数
ES6中提供了不定参数(rest parameters)特性来提供更好的实现方案。
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;
}
Note
函数的 length 属性统计的是函数命名参数的数量,不定参数的加入不会影响 length 属性的值。上述 pick 方法的 length 值为1.
不定参数的使用限制
- 每个函数最多只能声明一个不定参数,而且一定要放在所有参数的末尾;
- 不定参数不能用于对象字面量 setter 之中。
不定参数对 arguments 对象的影响
无论是否使用不定参数,arguments 对象总是包含所有传入函数的参数。
增强的 Function 构造函数
使用 Function 构造函数创建函数。
var add = new Function("first", "second", "return first + second");
console.log(add(1, 1)); // 2
创建的 add 方法是如下样子的:
ƒ anonymous(first,second
/*``*/) {
return first + second
}
ES6中增强了该构造函数,使其可以支持默认参数和不定参数。
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
展开运算符
在所有新功能中,与不定参数最相似的是展开运算符。
以Math.max()方法为例,该方法可以接受任意数量的参数并返回最大的那一个。
let value1 = 25,
value2 = 50;
Math.max(value1, value2); // 50
但该方法不支持数组,如果需要从数组中挑出一个最大的时该怎么做呢?
ES5中可以使用apply()方法实现该功能。
let values = [25, 50, 75, 100];
console.log(Math.max.apply(Math, values)); // 100
虽然可以实现该功能,但是难以理解。
ES6中可以使用展开运算符(...)简化上述示例。
let values = [25, 50, 75, 100];
console.log(Math.max(...values)); // 100
也可以将展开运算符与其它正常传入的参数混合使用。
let values = [-25, -50, -75, -100];
console.log(Math.max(...values, 0)); // 0
name 属性
ES6中所有的函数的name属性都有一个合适的值。可以帮助开发更好的追踪问题。
function doSomething() {
}
var doAnotherThing = function() {
};
console.log(doSomething.name); // 函数名称 "doSomething"
console.log(doAnotherThing.name); // 匿名函数的变量的名称 "doAnotherThing"
name 属性的特殊情况
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"
- doSomething.name
函数本身的名字权重更高 - person.sayName.name
其值取自对象字面量 - person.firstName.name
person.firstName实际上是个getter函数,自动加上了前缀 “get”。
setter函数也有其前缀“set”。
另外通过 bind() 函数创建的函数,其名称带有“bound”前缀;
通过Function构造函数创建的函数,其名称带有“anonymous”前缀。
var doSomething = function() {
};
console.log(doSomething.bind().name); // "bound doSomething"
console.log((new Function()).name); // "anonymous"
明确函数的多重用途
ES5及早期版本中的函数具有多重功能,可以结合new使用,函数内的this值将指向一个新对象,函数最终返回这个新对象。
function Person(name) {
this.name = name;
}
var person = new Person("JiaJia");
var notAPerson = Person("JiaJia");
console.log(person); // "Person {name: "JiaJia"}"
console.log(notAPerson); // "undefined"
console.log(window.name); // "JiaJia"
如果不同new关键字调用Person方法,不仅得不到想要的结果,还会在全局作用创建一个name属性。
在ES5中判断函数被调用的方法
使用 instanceof
操作符判断是会否是通过new关键字调用。
function Person(name) {
if (this instanceof Person) {
this.name = name;
} else {
throw new Error("必须通过new关键字来调用Person。");
}
}
var person = new Person("JiaJia");
var notAPerson = Person("JiaJia"); // 抛出错误
一般来说上述写法是有效的,但是也有例外情况。
因为有一种不依赖new关键字的方法也可以将this绑定到person的实例上。
function Person(name) {
if (this instanceof Person) {
this.name = name;
} else {
throw new Error("必须通过new关键字来调用Person。");
}
}
var person = new Person("JiaJia");
var notAPerson = Person.call(person, "XKA");
// 没有抛出错误,但也没有得到想要的对象
// 实际修改的是person实例的值
元属性(Metaproperty) new.target
为了解决判断函数是否通过new关键字调用的问题,ES6引入了 new.target 这个元属性。
元属性是指非对象的属性,其可以提供非对象目标的补充信息。
当调用函数的[[Construct]]方法时,new.target 被赋值为new操作符的目标,通常是新创建对象实例,也就是函数体内this的构造函数;
如果调用[[call]]方法,则new.target的值为undefined。
function Person(name) {
if (typeof new.target !== "undefined") {
this.name = name;
} else {
throw new Error("必须通过new关键字来调用Person。");
}
}
var person = new Person("JiaJia");
var notAPerson = Person.call(person, "XKA"); // 抛出错误
也可以检查 new.target 是否被某个特定构造函数所调用
function Person(name) {
if (typeof new.target === Person) {
this.name = name;
} else {
throw new Error("必须通过new关键字来调用Person。");
}
}
function AnotherPerson(name) {
Person.call(this, name);
}
var person = new Person("JiaJia");
var anotherPerson = new AnotherPerson("DLPH"); // 抛出错误
Note
在函数外使用 new.target 是一个语法错误。
块级函数
在代码块中声明的函数。
"use strict";
if (true) {
console.log(typeof doSomething); // "function"
function doSomething() {
}
doSomething();
}
console.log(typeof doSomething); // "undefined"
ES6严格模式下,在定义函数的代码块内,块级函数会被提升至顶部。
块级函数与let函数表达式类似,一旦执行过程流出了代码块,函数定义立即被移除。
两者的区别是let定义的函数不会被提升。
"use strict";
if (true) {
console.log(typeof doSomething); // 抛出错误
// Uncaught ReferenceError: doSomething is not defined
let doSomething = function () {
}
doSomething();
}
console.log(typeof doSomething);
非严格模式下的块级函数
在ES6的非严格模式下,块级函数会被提升至外围函数或全局作用域的顶部。
if (true) {
console.log(typeof doSomething); // "function"
function doSomething() {
}
doSomething();
}
console.log(typeof doSomething); // "function"
箭头函数
箭头函数是一种使用箭头(=>)定义函数的新语法,但是它与传统的JS函数有些不同。
- 没有 this、super、arguments和new.target绑定
- 不能通过new关键字调用
- 没有原型
- 不可以改变 this 的绑定
- 不支持 arguments 对象
- 不支持重复的命名参数
箭头函数语法
let reflect = value => value;
// 实际相当于
let reflect = function(value) {
return value;
};
如果要传入两个或以上参数,要在参数的两侧添加一对小括号。
let sum = (num1, num2) => num1 + num2;
// 实际上相当于
let sum = function(num1, nume) {
return num1 + num2;
};
如果没有参数,也要在声明的时候写一组没有内容的小括号。
let getName = () => "JiaJia";
// 实际上相当于
let getName = function() {
return "JiaJia";
};
如果函数体是多行,则需要用花括号包裹函数体。
let sum = (num1, num2) => {
return num1 + num2;
}
// 实际上相当于
let sum = function(num1, nume) {
return num1 + num2;
};
除了 arguments 对象不能使用外,某种程度上你都可以将花括号里的代码视作传统的函数体定义。
如果想创建一个空函数,需要写一对没有内容的花括号。
let doNothing = () => {};
// 实际上相当于
let doNothing = function() {};
如果想在箭头函数外返回一个对象字面量,则需要将该字面量包裹在小括号里。
let getTempItem = id => ({ id: id, name: "Temp" });
// 实际上相当于
let getTempItem = function(id) {
return {
id: id,
name: "Temp"
};
};
创建立即执行函数表达式
let person = ((name) => {
return {
getName: function() {
return name;
}
};
})("JiaJia");
console.log(person.getName()); // "JiaJia"
箭头函数没有this绑定
箭头函数中没有this绑定,必须通过查找作用域链来决定其值。
如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this;
否则this的值会被设置为undefined。
let PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log("Handling " + type + " fro " + this.id);
}
};
这里addEventListener的第二个参数如果使用匿名函数的形式,则this是当前click事件目标对象(这里是document)的引用。
而在本例中,this就是PageHandler对象。
箭头函数缺少正常函数所拥有的property属性,所以不能用它来定义新的类型。
如果尝试用new关键字调用一个箭头函数,会导致程序抛出错误。
var MyType = () => {};
var object = new MyType(); // 抛出错误
// Uncaught TypeError: MyType is not a constructor
箭头函数中的this值取决于该函数外部非箭头函数的this值,且不能通过call()、apply()或bind()方法来改变this的值。
箭头函数和数组
箭头函数的语法简洁,非常适用于数组处理。
var result = values.sort(function(a, b) {
return a - b;
});
可以简化为如下形式
var result = values.sort((a, b) => a - b);
箭头函数没有arguments绑定
箭头函数没有自己的arguments绑定,且无论函数在哪个上下文中执行,箭头函数始终可以访问外围函数的arguments对象。
function createArrowFunctionReturningFirstArg() {
return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction); // 5
尾调用优化
尾调用指的是函数作为另一个函数的最后一条语句被调用。
function doSomething() {
return doSomethingElse(); // 尾调用
}
在ES5中,尾调用的实现与其它函数调用的实现类似:创建一个新的栈帧(stack frame),将其推入调用栈来表示函数调用。
也就是说,在循环调用中,每一个未用完的栈帧都会保存在内存中,当调用栈变得过大时会造成程序问题。
ES6中的尾调用优化
ES6缩减了严格模式下尾调用栈的大小(非严格模式下不受影响),如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧。
- 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)
- 在函数内部,尾调用是最后一条语句
- 尾调用的结果作为函数返回
"use strict";
function doSomething() {
// 可优化
return doSomethingElse(); // 尾调用
}
以下形式均无法优化
"use strict";
function doSomething() {
// 不可优化
doSomethingElse();
}
"use strict";
function doSomething() {
// 不可优化
return 1 + doSomethingElse();
}
"use strict";
function doSomething() {
// 不可优化
var result = doSomethingElse();
return result;
}
"use strict";
function doSomething() {
var num = 1,
func = () => num;
// 不可优化,该函数是一个闭包
return func();
}
如何利用尾优化
递归函数是主要的应用场景,此时尾调用优化的效果最显著。
优化前:
function factorial(n) {
if (n <= 1) {
return 1;
} else {
// 无法优化,必须在返回之后执行乘法操作
return n * factorial(n - 1);
}
}
优化后:
function factorial(n, p = 1) {
if (n <= 1) {
return 1 * p;
} else {
return factorial(n - 1, n * p);
}
}
Warning
通过在谷歌浏览器上测试,好像没有起作用,优化后的代码依然会栈溢出。
这本书作者在写时,这个特性仍在审核中。估计是该优化没有通过ES6的审核。