上一章节中主要讲解了
- 处理无命名参数
- 增强的Function构造函数
- 展开运算符
- name属性
- 明确函数的多重用途
- 块级函数
本章节内容
- 箭头函数
- 尾调用优化
箭头函数
箭头函数有以下几个方面的特点:
- 没有 this, superm arguments and new.target 绑定。箭头函数中的 this, super, arguments和arguments的值由外围最近一层包含它的非箭头函数定义。
- 不同通过new关键字调用。箭头函数内部没有 [[construct]]方法, 因此不能当作构造器,使用new操作符;
- 不存在原型(No prototype),由于不可以通过new关键字调用箭头函数,因而没有构建原型的需求,所以箭头函数不存在prototype这个属性;
- 不能改变this, 在整个箭头函数生命周期this值保持不变;
- 不存在arguments对象,不过包含它的函数存在,箭头函数依靠命名参数和rest parameters访问函数的参数;
- 不能拥有重复的命名参数,ES5只有严格模式下才不允许;
箭头函数语法
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
// 当要传入多参数时,要在参数两个添加一对小括号
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
// 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分
var f = () => 5;
// 等同于
var f = function () { return 5 };
// 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。
var sum = (num1, num2) => {num1++; return num1 + num2; }
// 等同于
var sum = function(num1, num2) {
num1++;
return num1 + num2;
};
// 如果想创建一个空函数,需要写一对儿没有内容的花括号
let getTempItem = () => { };
// 等同于
let getTempItem = function() = { };
// 如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。
let fn = () => void doesNotReturn();
如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
// 等同于
let getTempItem = function(id) {
return {
id : id,
name : "Temp"
};
};
下面是一种特殊情况,虽然可以运行,但会得到错误的结果。
let foo = () => { a: 1 };
foo() // undefined
上面代码中,原始意图是返回一个对象{ a: 1 }
,但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1
。这时,a
可以被解释为语句的标签,因此实际执行的语句是1;
,然后函数就结束了,没有返回值。
const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
return person.first + ' ' + person.last;
}
箭头函数可以与变量解构结合使用。
const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
return person.first + ' ' + person.last;
}
创建立即执行函数表达式
当想创建一个与其他程序隔离的作用域时,可以定义一个匿名函数并立即调用,自始至终不保存对该函数的引用。
let person=function(name){
return{
getName: function(){
return name;
}
}
}("Baby");
console.log(person.getName());// "Baby"
立即执行函数表达式创建了一个包含getName()方法的新对象,将参数name作为该对象的一个私有成员返回给函数的调用者
只要将箭头函数包裹在小括号里,能够实现相同的行为。
let person=((name) => {
return{
getName: function(){
return name;
}
};
})("Baby");
console.log(person.getName());// "Baby"
箭头函数没有this绑定
箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。所以箭头函数可以让this指向固定化。
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数
setInterval(() => this.s1++, 1000);
// 普通函数
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。
箭头函数可以让this指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。然而,不使用箭头函数的情况下代码会报错。
let 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);
}
};
// init函数中的this.doSomething,this指向的是函数内部document对象;
// 而不是PageHandler对象,document对象中不存在doSomething,所以无法正常执行
使用bind()方法显式的将函数的this绑定到PageHandler上来修正这个问题。
let PageHandler = {
id : "123",
init : function(){
document.addEventListener("click", (function(event) {
this.doSomething(event.type);
}).bind(this), false);
},
doSomething : function(type) {
console.log("Handle "+ type + " for " + this.id);
}
}
// 调用bind(this)后创建了一个新函数,它的this被绑定到当前的this,也就是PageHandler
为了避免创建一个额外的函数,可以使用箭头函数。
let PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click",
event => this.doSomething(evnet.type), false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
// 此处箭头函数没有自己this,它的this就是其外部函数的this,即init()的this;
// init为PageHandler的方法,this指向PageHandler对象实例
箭头函数不能使用new
var MyType = () => {};
var obj = new MyType(); // Error,不可以通过new关键字调用箭头函数
箭头函数和数组
诸如sort()、map()、reduce()这些可以接受回调函数的数组方法,可以通过箭头函数简化。
// 正常函数写法
[1,2,3].map(function (x) {
return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);
// 正常函数写法
var result = values.sort(function (a, b) {
return a - b;
});
// 箭头函数写法
var result = values.sort((a, b) => a - b);
箭头函数没有arguments绑定
箭头函数没有arguments对象,但是可以使用包含函数中的arguments对象。
function createArrowFunctionReturningFirstArg() {
// arguments 为 createArrowFunctionReturningFirstArg中的对象
return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(10);
arrFunction(); // 10
箭头函数的辨识方法
// 箭头函数使用typeof和instanceof操作符调用 与其他函数一样
var sum = (num1, num2) => num1 + num2;
console.log(typeof sum);// "function"
console.log(sum instanceof Function);// true
可以在箭头函数上调用call()、apply()、bind()方法,与其他函数不同的是,箭头函数的this不会受到这些方法影响。
var sum = (num1, num2) => num1 + num2;
console.log(sum.call(null, 1, 2));
console.log(sum.apply(null, [1, 2]));
var boundSum = sum.bind(null, 1, 2);
console.log(boundSum());
尾调用优化
尾调用(Tail Call)是指某个函数的最后一步是调用另一个函数。
function f(x){
return g(x); // 尾调用
}
以下三种情况,都不属于尾调用。
// 情况一,因为调用函数g之后,还有赋值操作,所以不属于尾调用
function f(x){
let y = g(x);
return y;
}
// 情况二,原因同情况一
function f(x){
return g(x) + 1;
}
// 情况三,原因是没有返回函数g
function f(x){
g(x);
}
尾调用不一定出现在函数尾部,只要是最后一步操作即可
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
上面代码中,函数m和n都属于尾调用,因为它们都是函数f的最后一步操作。
ES6中的尾调用优化
尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函A
的内部调用函数B
,那么在A
的调用帧上方,还会形成一个B
的调用帧。等到B
运行结束,将结果返回到A
,B
的调用帧才会消失。如果函数B
内部还调用函数C
,那就还有一个C
的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
上面代码中,如果函数g
不是尾调用,函数f
就需要保存内部变量m
和n
的值、g
的调用位置等信息。但由于调用g
之后,函数f
就结束了,所以执行到最后一步,完全可以删除f(x)
的调用帧,只保留g(3)
的调用帧。
这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
function addOne(a){
var one = 1;
function inner(b){
return b + one;
}
return inner(a);
}
上面的函数不会进行尾调用优化,因为内层函数inner
用到了外层函数addOne
的内部变量one
。
利用尾调优化--尾递归
函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。
如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
-
func.arguments
:返回调用时函数的参数。 -
func.caller
:返回调用当前函数的那个函数。
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
function restricted() {
'use strict';
restricted.caller; // 报错
restricted.arguments; // 报错
}
restricted();
网友评论