第10章 函数
第10章 函数
函数是对象,每个函数都是Function类型的实例,和其他引用类型一样拥有属性和方法。
因为函数是对象,函数名称是指向函数对象的指针。
函数表达式最后需要加分号,就像变量声明一样:
let sum = function (num1,num2){
return num1 + num2;
};
console.log(sum(3,2));//5
箭头函数
ES6中任何可以使用函数表达式的地方都可以使用箭头函数:
let arrowSum = (a, b) => {
return a + b;
};
let functionExpressionSum = function(a, b) {
return a + b;
};
console.log(arrowSum(5, 8)); // 13
console.log(functionExpressionSum(5, 8)); // 13
只有一个参数时圆括号可以省略,0个或多个参数不可省略:
let double = (x) => {
return 2 * x;
};
let triple = x => {
return 3 * x;
};
大括号也可以省略,此时不可使用return:
let doublex = x =>{return 2 * x};
let triplex = x => 3 * x;
console.log(doublex(2));//4
console.log(triplex(3));//9
let value = {}
let setName = x => x.name = 'Ciri';
setName(value);
console.log(value.name);//Ciri
let multiply =(a,b) => a * b;
console.log(multiply(2,3));//6
//let sum = (a,b) => return a + b;//SyntaxError: Unexpected token 'return'
箭头函数不允许使用arguments,super,new.target,也不能作为构造函数,并且没有原型 。
函数名
ES6里所有函数对象暴露一个name属性:
function foo() {}
let bar = function() {};
let baz = () => {};
console.log(foo.name); // foo
console.log(bar.name); // bar
console.log(baz.name); // baz
console.log((() => {}).name); // (empty string)
console.log((new Function()).name); // anonymous
如果是getter,setter,或使用bind()实例化的函数,会加前缀以识别:
function foo() {}
console.log(foo.bind(null).name); // bound foo
let dog = {
years: 1,
get age() {
return this.years;
},
set age(newAge) {
this.years = newAge;
}
}
let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age');
console.log(propertyDescriptor.get.name); // get age
console.log(propertyDescriptor.set.name); // set age
bind()方法创建一个新的函数,在bind()被调用时,这个新函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
理解参数
ECMAScript中的函数参数的行为方式与大多数其他语言中的函数参数不同。 ECMAScript函数不在乎传入的参数数量,也不在乎这些参数的数据类型。定义了一个接受两个参数的函数并不意味着只能传递两个参数。也可以输入一个、三个或不输入。
之所以如此,是因为ECMAScript中的参数在内部表示为数组。数组始终传递给函数,但函数并不关心数组中的内容(如果有)。实际上,当使用function关键字(意味着非箭头函数)定义函数时,实际上存在一个arguments对象,可以在函数内部访问该对象,以获取传入的每个参数的值。
arguments对象的行为类似于数组(尽管它不是Array的实例),因为可以使用方括号表示法访问每个参数(第一个参数是arguments[0],第二个参数是arguments[1],依此类推)并使用length属性确定传入了多少个参数。
arguments中的值总是与命名参数同步:
function doAdd(num1, num2) {
arguments[1] = 10;
console.log(arguments[0] + num2);
}
doAdd(6, 6); //16
doAdd(6); //NaN
此例中doAdd()方法总是覆写第二个参数值为 10 ,因为arguments对象中的值总是反映到相应命名参数。对arguments [1]的更改也会更改num2的值,因此两者的值均为 10 。但这并不意味着两者访问相同内存空间,它们的内存空间是分开的,但是保持同步。另外,如果只传递一个参数,设置arguments[1]的值并不能同步到命名参数,因为arguments对象的length取决于传入的参数个数。严格模式下会报错。
箭头函数中的arguments
箭头函数内不能使用arguments关键字获取参数。
没有重载
如果定义了两个同名函数,则函数名归最后一个所有:
function addSomeNumber(num) {
return num + 100;
}
function addSomeNumber(num) {
return num + 200;
}
console.log(addSomeNumber(100));//300
默认形参值
当使用默认形参时,arguments对象的值不反映形参的默认值:
function namedArg(name = 'Ciri',age){
console.log(name === arguments[0]);
console.log(age === arguments[1]);
name = 'Geralt';
age = 100;
console.log(arguments[0]);
console.log(arguments[1]);
console.log(name);
console.log(age);
}
namedArg();//false true undefined undefined Geralt 100
namedArg('Triss',99);//true true Triss 99 Geralt 100
传递undefined为参数时等于没有传递参数:
function beKing(name = 'Ciri'){
console.log(`king ${name}`);
}
beKing(undefined);//king Ciri
延展实参和剩余形参
延展实参
与其将单个数组作为实参传递,不如将数组分解并作为单独的实参进行传递。
将延展操作符(...)用于可迭代对象,该对象将被分解成N个单独的实参传递给函数。
let values = [1, 2, 3, 4];
function getSum() {
let sum = 0;
for (let i = 0; i < arguments.length; ++i) {
sum += arguments[i];
}
return sum;
}
console.log(getSum.apply(null,values));//10
console.log(getSum(...values));//10
console.log(getSum(-2,...values));//8
console.log(getSum(-3,...values,666));//673
arguments对象并不知晓延展操作符的存在,而是将其视为N个单独的参数:
let values = [1,2,3,4];
function countArgs(){
console.log(arguments.length);
}
countArgs(...values,6,7,8);//7
countArgs(-5,...values,...[5,6,7,8,9]);//10
标准函数和箭头函数都可用:
function getProduct(a, b, c = 1) {
return a * b * c;
}
let getSum = (a, b, c = 0) => {
return a + b + c;
}
console.log(getProduct(...[1, 2])); // 2 1*2*1
console.log(getProduct(...[1, 2, 3])); // 6 1*2*3
console.log(getProduct(...[1, 2, 3, 4])); // 6 1*2*3
console.log(getSum(...[0, 1])); // 1 0+1+0
console.log(getSum(...[0, 1, 2])); // 3 0+1+2
console.log(getSum(...[0, 1, 2, 3])); // 3 0+1+2
剩余形参
延展操作符用在函数定义时,剩余形参将变成Array对象:
function getSum(...values){
console.log(values);//[1, 2, 3]
return values.reduce((x,y)=>x+y,0);
}
console.log(getSum(1,2,3));//6
剩余形参只能放在形参列表的最后。
箭头函数支持剩余形参:
let getSum = (...values) => {
return values.reduce((x, y) => x + y, 0);
}
console.log(getSum(1, 2, 3)); // 6
剩余形参不影响arguments对象:
function getSum(...values) {
console.log(arguments.length); // 3
console.log(arguments); // [1, 2, 3]
console.log(values); // [1, 2, 3]
}
getSum(1, 2, 3);
函数声明VS函数表达式
二者区别在于Javascript引擎将数据加载到执行环境的方式。在执行任何代码之前,函数声明可读且在执行环境中。
函数表达式不会提升:
console.log(getSum(3,2));//5
function getSum(a,b){
return a + b;
}
console.log(sum(2,3));
//ReferenceError: Cannot access 'sum' before initialization
let sum = function(a,b){
return a + b;
}
函数也是值
在ECMAScript中函数名只不过是变量而已,变量能使用的地方函数名也可以。这意味着不仅可以将一个函数作为参数传递给另一个函数,还可以将一个函数作为另一个函数的结果返回。
函数内部
在ECMAScript 5中,函数内部存在两个特殊对象:arguments和this。在ECMAScript 6中,引入了new.target属性。
arguments
arguments对象是一个类数组对象,只有使用function关键字声明的函数才有。arguments对象有一个名为callee的属性,该属性是指向拥有arguments对象的函数的指针。
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
//与factorial解耦,当函数名改变时也可正常计算
}
}
console.log(factorial(10));//3628800
this
this在标准函数内指代操纵此函数的环境对象(如在网页的全局作用域内调用函数时,this对象指向window):
window.color = 'red';
let o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'blue'
在箭头函数内,this指代箭头函数定义时的环境对象 :
window.color = 'red';
let o = {
color: 'blue'
};
let sayColor = () => console.log(this.color);
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'red'
这在当事件或Timeout将在回调中调用函数而调用对象不是预期对象的情况下非常有用。箭头函数中this指代的环境对象被保护的很好:
function King() {
this.royaltyName = 'Henry';
// this将是King实例
setTimeout(() => console.log(this.royaltyName), 1000);
}
function Queen() {
this.royaltyName = 'Elizabeth';
console.log(this.royaltyName); //Elizabeth
// this将是window对象
setTimeout(function() {
console.log(this.royaltyName);
}, 1000);
}
new King(); // Henry
new Queen(); // undefined
caller
ES5在函数对象上添加了一个属性:calller,其中包含对调用此函数的函数的引用;如果从全局作用域调用了该函数,则为null。
function outer() {
inner();
}
function inner() {
console.log(inner.caller);
}
outer();//函数outer()
松藕可使用arguments.callee.caller替换:
function outer() {
inner();
}
function inner() {
console.log(arguments.callee.caller);
console.log(arguments.caller);//undefined
}
outer();//函数outer()
严格模式下访问arguments.callee将报错,ES5定义了arguments.caller,严格模式也报错,非严格模式为undefined。
new.target
如果函数使用new关键字调用,则new.target将引用构造函数或函数,如果不使用new调用函数,new.target为undefined:
function King() {
if (!new.target) {
throw `King必须使用new实例化`;
}
console.log(`King使用new实例化`);
console.log(new.target); //ƒ King() {}
}
new King(); // King使用new实例化
King(); //Uncaught King必须使用new实例化
函数属性和方法
每个函数拥有两个属性:lenght(命名参数的个数)和prototype :
function a(m,n,...o){}
function b(){}
console.log(a.length);//2
console.log(b.length);//0
在ES5里prototype属性不可枚举,所以不会出现在for...in循环中。
apply()和call()使用指定的this值调用函数。apply()接受两个参数:函数内部的this值和一组参数,第二个参数可以是数组实例,也可以是arguments对象。
window.color = 'red';
let o = {
color: 'dark'
};
function getColor() {
console.log(this.color);
}
getColor.apply(this, null); //red
o.getColor1 = function() {
getColor.apply(this, null);
}
o.getColor1(); //dark
o.getColor2 = getColor;
o.getColor2.apply(this, null); //red
getColor.apply(o, null); //dark
call()方法第一个参数为this,其余参数必须是单独的具体参数。
通过参数指定this值:
window.color = 'red';
let o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue
ES5定义的bind(),该方法创建一个新的函数实例,其this值绑定到传递给bind()的值:
window.color = 'red';
var o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
let objectSayColor = sayColor.bind(o);
objectSayColor(); // blue
函数表达式
递归
典型的递归格式如下:
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
使用命名函数表达式(named function expressions),可解决arguments.callee不能在严格模式下使用的问题:
const factorial = function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1);
}
};
let anotherFactorial = factorial;
console.log(anotherFactorial(4));//24
尾调优化
ES6规范还引入了内存管理优化功能,该功能允许JavaScript引擎在满足某些条件时重用栈帧。具体来说,此优化与“尾部调用”有关,其中外部函数的返回值也是内部函数的返回值。如下所示:
function outerFunction() {
return innerFunction(); // tail call
}
在ES6优化之前,上例执行时在内存中发生如下情况:
执行到达outerFunction时,第一个栈帧入栈
outerFunction执行到达return时,为计算返回值,必须计算innerFunction
执行到达innerFunction时,第二个栈帧入栈
innerFunction执行,计算返回值
innerFunction的返回值传递给outerFunction,然后再由outerFunction返回之
栈帧弹出
ES6优化如下:(区别少一个栈帧层数)
执行到达outerFunction时,第一个栈帧入栈
outerFunction执行到达return时,为计算返回值,必须计算innerFunction
引擎意识到第一个栈帧可以安全的弹出,因为innerFunction的返回值也是outerFunction的返回值
outerFunction的栈帧弹出
执行到达innerFunction,栈帧入栈
innerFunction执行,计算返回值
innerFunction栈帧弹出
尾调优化要求
只有外部栈帧不再需要时才执行此优化,但需要满足如下条件:
必须在严格模式下
外部函数的返回值是调用尾调函数
尾调函数返回后无其他执行
尾调函数不是引用外部函数作用域变量的闭包
以下不会发生尾调优化:
"use strict";
// 未优化,尾调函数未返回
function outerFunction() {
innerFunction();
}
// 未优化,尾调未直接返回
function outerFunction() {
let innerFunctionResult = innerFunction();
return innerFunctionResult;
}
// 未优化,尾调后执行了toString()
function outerFunction() {
return innerFunction().toString();
}
// 未优化,尾调是闭包
function outerFunction() {
let foo = 'bar';
function innerFunction() {
return foo;
}
return innerFunction();
}
下面会优化:
"use strict";
// 已优化,参数计算发生在栈帧丢弃前
function outerFunction(a, b) {
return innerFunction(a + b);
}
//已优化,最先的返回值不影响栈帧
function outerFunction(a, b) {
if (a < b) {
return a;
}
return innerFunction(a + b);
}
// 已优化,两个内部函数都被认为是尾调
function outerFunction(condition) {
return condition ? innerFunctionA() : innerFunctionB();
}
尾调优化案例
计算斐波那契数:
function fib(n) {
if (n < 2) {
return n;
}
return fib(n-1) + fib(n-2);
}
console.log(fib(0)); // 0
console.log(fib(1)); // 1
console.log(fib(2)); // 1
console.log(fib(3)); // 2
console.log(fib(4)); // 3
console.log(fib(5)); // 5
console.log(fib(6)); // 8
尾调优化版:
"use strict";
// base case
function fib(n) {
return fibImpl(0, 1, n);
}
// recursive case
function fibImpl(a, b, n) {
if (n === 0) {
return a;
}
return fibImpl(b, a + b, n - 1);
}
console.log(fib(1000));//4.346655768693743e+208
闭包
闭包就是函数访问其他函数作用域内的变量。
作用域链的创建和使用对于理解闭包非常重要:当函数调用时,其执行环境被创建,然后作用域链被创建,函数的激活对象以arguments对象和命名参数初始化,外部函数的激活对象是此作用域链的第二个对象;以此类推,直到全局执行环境的变量对象。
随着函数的执行,将在作用域链上查找变量和读写值,参考如下代码:
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let result = compare(5, 10);//-1
上面代码在全局执行环境定义了一个叫compare的函数,当compare()第一次调用时,一个新激活对象将创建,其包含arguments、命名参数value1和value2。全局执行环境的变量对象是compare执行环境作用域链的第二个对象,它包含this,compare,result。下图展示了它们的关系:
幕后,每个执行环境都有一个变量对象。全局执行环境的变量对象始终存在。然而局部环境变量对象如compare()的变量对象,只有函数执行时才存在。当compare()定义时,其作用域链被创建,预加载了全局变量对象,并保存在内部[[scope]]属性里。当函数调用时,其执行环境被创建,执行环境的作用域链通过复制函数内部[[scope]]属性创建。随后,创建激活对象(也就是变量对象)并添加到执行环境作用域链的前面。作用域链本质上是包含变量对象的指针。
每当在函数内部访问变量时,将在作用域链上搜索具有该变量名的变量,一旦函数调用完成,局部变量也随之销毁,仅留下全局作用域链在内存中。
然而,闭包表现形式不同。参考如下代码:
function createComparisonFunction(propertyName) {
return function(object1, object2) {
let value1 = object1[propertyName];
let value2 = object2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
};
}
let compare = createComparisonFunction('name');
let result = compare({ name: 'Nicholas' }, { name: 'Matt' });
在createComparisonFunction()中,匿名函数的作用域链包含createComparisonFunction()激活对象的引用。当匿名函数从createComparisonFunction()返回时,其作用域链包含createComparisonFunction()的激活对象和全局变量对象,这使得匿名函数可以访问createComparisonFunction()的所有变量。
另一个有趣的副作用是,一旦createComparisonFunction()执行完毕其激活对象不能销毁,因为匿名函数的作用域链仍然保留其引用。
当createComparisonFunction()执行完毕,其作用域链被销毁,但其激活对象仍然存在直到匿名函数销毁。
当上例最后两行执行时:
this对象
在闭包中使用this对象会引入一些复杂的行为。当一个函数不是使用箭头函数定义时,this对象在运行时根据函数执行环境进行绑定:当在全局函数内时,非严格模式this为window,严格模式为undefined。当作为对象方法调用时,this为该对象。
匿名函数不绑定到对象,this为window 除非在严格模式下(为undefined)。
闭包行为不太一样:
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentityFunc() {
return function f() {//匿名和非匿名返回值一样
return this.identity;
};
}
};
console.log(object.getIdentityFunc()()); // The Window
每个函数调用时自动获得两个特殊的变量:arguments和this,内部函数无法直接访问它们,但是可通过将this赋值给另一个变量进行访问:
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentityFunc() {
let that = this;
return function f() {
return that.identity;
};
}
};
console.log(object.getIdentityFunc()()); // My Object
this值的几个特例:
window.identity = 'The Window';
let object = {
identity: 'My Object',
getIdentity() {
return this.identity;
}
};
console.log(object.getIdentity()); // 'My Object'
console.log((object.getIdentity)()); // 'My Object'
console.log((object.getIdentity = object.getIdentity)()); // 'The Window'
输出的第一行和平常一样;第二行this值保持不变因为object.getIdentity 和 (object.getIdentiy)相等;第
三行执行赋值操作,并调用结果,由于此赋值表达式的值是函数本身,因此this不会保留。
立即调用函数表达式(IIFE)
立即调用的匿名函数通常叫做IIFE(IMMEDIATELY INVOKED FUNCTION EXPRESSIONS),它有点像函数声明,但是由于它包含在括号中,因此被解释为函数表达式,它通过第二对括号调用,基本语法如下:
(function() {
// block code here
})();
在ES6之前模拟块级作用域:
// IIFE
(function() {
for (var i = 0; i < 10; i++) {
console.log(i);
}
})();
console.log(i); // ReferenceError: i is not defined
另一个经典用法是冻结形参值,但ES6直接使用let既可:
for (let i = 0; i < 10; ++i) {
setTimeout(() => console.log(i), 1000);//0~10正常
}
for(var i = 0;i < 10;++i){
setTimeout(()=>console.log(i),1000);//输出都是10
}
for (var i = 0; i < 10; ++i) {
setTimeout((() => console.log(i))(), 1000);//0~10正常
}
let i = 0;
for (i; i < 10; ++i) {
setTimeout(() => console.log(i), 1000);//全是10
}
私有变量
任何定义在函数内或块内的变量可以认为是私有的,这包括函数参数,局部变量,和定义在函数内的函数。如下:
function add(num1, num2) {
let sum = num1 + num2;
return sum;
}
在此函数中,有三个私有变量:num1,num2和sum。这些变量可以在函数内部访问,但不能在函数外部访问。如果要在此函数内部创建闭包,则它将可以通过其作用域链访问这些变量。可创建可以访问这些私有变量的公共方法。
特权方法是可以访问私有变量或私有函数的公共方法。有两种在对象上创建特权方法的方式。第一种是在构造函数内:
function MyObject() {
// 私有变量和函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特权方法
this.publicMethod = function() {
privateVariable++;
return privateFunction();
};
}
let mo = new MyObject();
console.log(mo.privateVariable); //undefined
该模式定义了构造函数内部的所有私有变量和函数。然后可以创建特权方法来访问那些私有成员。之所以可行,是因为特权方法在构造函数中定义时会变为闭包,可以完全访问构造函数范围内定义的所有变量和函数。一旦创建MyObject的实例后,将无法直接访问privateVariable和privateFunction();只能通过publicMethod()访问。
可使用私有成员和特权成员来隐藏那些不可直接更改的数据,如下所示:
function Person(name) {
this.getName = function() {
return name;
};
this.setName = function(value) {
name = value;
};
}
let person = new Person('Ciri');
console.log(person.getName()); // Ciri
person.setName('Geralt');
console.log(person.getName());//Geralt
console.log(person.name);//undefined
这段代码中的构造函数定义了两个特权方法:getName()和setName()。每个方法都可以在构造函数外部访问,并访问私有name变量。在Person构造函数之外,无法访问name。因为这两个方法都是在构造函数中定义的,所以它们都是闭包,可以通过作用域链访问name。
静态私有变量
特权方法也可以通过使用私有作用域来定义私有变量或函数来创建:
(function() {
// 私有变量和函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 构造函数
MyObject = function() {};
// 公共特权方法
MyObject.prototype.publicMethod = function() {
privateVariable++;
return privateFunction();
};
})();
上例这个模式中,创建一个私有作用域来封装构造函数及其方法,构造函数使用的是函数表达式,因为 函数声明会创建局部函数 ,这不符合要求。 MyObject声明未使用任何关键字 ,将成为全局变量。
另外,这个模式的私有变量和函数在所有实例间共享,因为特权方法定义在原型上。
(function() {
let name = '';
Person = function(value) {
name = value;
};
Person.prototype.getName = function() {
return name;
};
Person.prototype.setName = function(value) {
name = value;
};
})();
let person1 = new Person('Ciri');
console.log(person1.getName()); // 'Ciri'
console.log(person.getName.name);//
person1.setName('Geralt');
console.log(person1.getName()); // 'Geralt'
let person2 = new Person('Triss');
console.log(person1.getName()); // 'Triss'
console.log(person2.getName()); // 'Triss'
上例name成为了静态私有变量,将在所有实例间共享。
模块模式
使用模块模式增强基本的单例模式:
let singleton = function() {
// 私有变量和函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 公共方法和属性
return {
publicProperty: true,
publicMethod() {
privateVariable++;
return privateFunction();
}
};
}();
模块模式使用一个匿名函数返回一个对象。匿名函数内,先定义私有变量和函数,然后使用一个对象字面量作为函数返回值,该对象仅包含需要公开的方法和属性。
本质上,返回的对象字面量定义了单例的公共接口。当单例需要某些初始化操作和访问私有变量时会非常有用,如下伪代码:
let application = function() {
// 私有变量和函数
let components = new Array();
// 初始化
components.push(new BaseComponent());
// 公共接口
return {
getComponentCount() {
return components.length;
},
registerComponent(component) {
if (typeof component == 'object') {
components.push(component);
}
}
};
}();
模块增强模式
当单例对象需要是特定类型的实例但必须通过其他属性或方法进行增强时,此模式很有用:
let singleton = function() {
// 私有变量和函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 创建对象
let object = new CustomType();
// 添加公共属性和方法
object.publicProperty = true;
object.publicMethod = function() {
privateVariable++;
return privateFunction();
};
// 返回该对象
return object;
}();
之前例子的application对象若必须是BaseComponent的实例时:
let application = function() {
// 私有变量和函数
let components = new Array();
// 初始化
components.push(new BaseComponent());
// 创建application的本地副本
let app = new BaseComponent();
// 公共接口
app.getComponentCount = function() {
return components.length;
};
app.registerComponent = function(component) {
if (typeof component == "object") {
components.push(component);
}
};
// 返回之
return app;
}();