在ECMAScript 5中,函数内部存在两个特殊的对象:arguments和this
ECMAScript 6又新增了new.target属性
10.9.1 arguments
arguments对象是一个类数组对象,包含调用函数时传入的所有参数。
这个对象只有以function关键字定义函数(相对于使用箭头语法创建函数)时才会有。
arguments对象有一个callee属性,指向arguments对象所在函数的指针。
来看下面这个经典的阶乘函数:
function factorial(num) {if (num <= 1) {return 1;} else {return num * factorial(num - 1);}}
阶乘函数一般定义成递归调用的,就像上面这个例子。
只要给函数一个名称,而且这个名称不会变,这样定义就没有问题。
但是,这个函数要正确执行就必须保证函数名是factorial,从而导致了紧密耦合。
使用arguments.callee,可以让函数逻辑与函数名解耦:
function factorial(num) {if (num <= 1) {return 1;} else {return num * arguments.callee(num - 1);}}
这个重写之后的factorial()函数,已经用arguments.callee代替了之前硬编码的factorial
这意味着无论函数叫什么名称,都可以引用正确的函数。
考虑下面的情况:
function factorial(num) {if (num <= 1) {return 1;} else {return num * arguments.callee(num - 1);}}let trueFactorial = factorial;factorial = function() {return 0;};console.log(trueFactorial(5)); // 120console.log(factorial(5)); // 0
trueFactorial变量被赋值为factorial,实际上把同一个函数的指针又保存到了另一个位置。
然后,factorial函数又被重写为一个返回0的函数。
如果像factorial()最初的版本那样不使用arguments.callee,那么像上面这样调用trueFactorial()就会返回0。
不过,通过将函数与名称解耦,trueFactorial()就可以正确计算阶乘,而factorial()则只能返回0。
10.9.2 this
另一个特殊的对象是this,它在标准函数和箭头函数中有不同的行为。
在标准函数中,this引用的是把函数当成方法调用的上下文对象,这时候通常称其为this值(在网页的全局上下文中调用函数时,this指向windows)。
来看下面的例子:
window.color = 'red';let o = {color: 'blue'};function sayColor() {console.log(this.color);}sayColor(); // redo.sayColor = sayColor;o.sayColor(); // blue
定义在全局上下文中的函数sayColor()引用了this对象。
这个this到底引用哪个对象必须到函数被调用时才能确定。因此这个值在代码执行的过程中可能会变。
如果在全局上下文中调用sayColor(),这结果会输出”red”,因为this指向window,而this.color相当于window.color。
而在把sayColor()赋值给o之后再调用o.sayColor(), this会指向o,即this.color相当于o.color,所以会显示”blue”。
在箭头函数中,this引用的是定义箭头函数的上下文。
下面的例子演示了这一点。
在对sayColor()的两次调用中,this引用的都是window对象,因为这个箭头函数是在window上下文中定义的:
window.color = 'red';let o = {color: 'blue'};let sayColor = () => console.log(this.color);sayColor(); // redo.sayColor = sayColor;o.sayColor(); // red
在事件回调或定时回调中调用某个函数时,this值指向的并非想要的对象。
此时将回调函数写成箭头函数就可以解决问题。
这是因为箭头函数中的this会保留定义该函数时的上下文:
function King() {this.royaltyName = 'Henry';// this引用King的实例setTimeout(() => console.log(this.royaltyName), 1000);}function Queen() {this.royaltyName = 'Elizabeth';// this引用window对象setTimeout(function() { console.log(this.royaltyName) }, 1000);}new King(); // Henrynew Queen(); // undefined
注:函数名只是保存指针的变量。
因此全局定义的sayColor()函数和o.sayColor()是同一个函数,只不过执行的上下文不同。
10.9.3 caller
ECMAScript 5给函数对象上添加一个属性:caller。
这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为null。
比如:
function outer() {inner();}function inner() {console.log(inner.caller);}outer();// ƒ outer() {// inner();// }// 显示outer()函数的源代码。这是因为ourter()调用了inner(), inner.caller指向outer()
如果要降低耦合度,则可以通过arguments.callee.caller来引用同样的值:
function outer() {inner();}function inner() {console.log(arguments.callee.caller);}outer();// ƒ outer() {// inner();// }
在严格模式下访问arguments.callee会报错。
ECMAScript 5也定义了arguments.caller,但在严格模式下访问它会报错,在非严格模式下则始终是undefined。这是为了分清arguments.caller和函数的caller而故意为之的。
而作为对这门语言的安全防护,这些改动也让第三方代码无法检测同一上下文中运行的其他代码。
严格模式下还有一个限制:不能给函数的caller属性赋值,否则会导致错误。
10.9.4 new.target
ECMAScript中的函数,始终可以作为构造函数实例化一个新对象,也可以作为普通函数被调用。
ECMAScript 6新增了,检测函数是否使用new关键字调用的new.target属性。
如果函数是正常调用的,则new.target的值是undefined;
如果是使用new关键字调用的,则new.target将引用被调用的构造函数。
function King() {if (!new.target) {throw 'King must be instantiated using "new"'}console.log('King instantiated using "new"')}new King(); // King instantiated using "new"King(); // 报错:Uncaught King must be instantiated using "new"
10.10 函数属性与方法
ECMAScript中的函数是对象,因此有属性和方法。
每个函数都有两个属性:length和prototype
其中,length属性保存函数定义的命名参数的个数
如下例所示:
function sayName(name) { console.log(name); }function sum(num1, num2) { return num1 + num2; }function sayHi() { console.log('hi'); }console.log(sayName.length); // 1console.log(sum.length); // 2console.log(sayHi.length); // 0
以上代码定义了3个函数,每个函数的命名参数个数都不一样。
sayName()函数有1个命名参数,所以其length属性为1。
sum()函数有两个命名参数,所以其length属性是2。
sayHi()没有命名参数,其length属性为0。
prototype是保存引用类型所有实例方法的地方。
这意味着toString()、valueOf()等方法实际上都保存在prototype上,进而由所有实例共享。
这个属性在自定义类型时特别重要。
在ECMAScript 5中,prototype属性是不可枚举的,因此使用for-in循环不会返回这个属性。
函数还有两个方法:apply()和call()。
这两个方法都会以指定的this值来调用函数,即会设置调用函数时函数体内this对象的值。
apply()方法接收两个参数:函数内this的值和一个参数数组。第二个参数可以是Array的实例,但也可以是arguments对象。
来看下面的例子:
function sum(num1, num2) {return num1 + num2;}function callSum1(num1, num2) {return sum.apply(this, arguments); // 传入arguments对象}function callSum2(num1, num2) {return sum.apply(this, [num1, num2]); // 传入数组}console.log(callSum1(10, 10)); // 20console.log(callSum2(10, 10)); // 20
在这个例子中,callSum1()会调用sum()函数,将this作为函数体内的this值(这里等于window,因为是在全局作用域中调用的)传入,同时还传入了arguments对象。callSum2()也会调用sum()函数,但会传入参数的数组。这两个函数都会执行并返回正确的结果。
注:在严格模式下,调用函数时如果没有指定上下文对象,则this值不会指向window。除非使用apply()或call()把函数指定给一个对象,否则this的值会变成undefined。
call()方法与apply()的作用一样,只是传参的形式不同。
第一个参数跟apply()一样,也是this值,而剩下的要传给被调用函数的参数则是逐个传递的。
换句话说,通过call()向函数传参时,必须将参数一个一个地列出来
比如:
function sum(num1, num2) {return num1 + num2;}function callSum(num1, num2) {return sum.call(this, num1, num2);}console.log(callSum(10, 10)); // 20
这里的callSum()函数必须逐个地把参数传给call()方法。结果跟apply()的例子一样。
到底是使用apply()还是call(),取决于怎么给要调用的函数传参更方便
如果想直接传arguments对象或者一个数组,那就用apply();
否则,就用call()
当然,如果不用给被调用的函数传参,则使用哪个方法都一样。
apply()和call()真正强大的地方并不是给函数传参,而是控制函数调用上下文即函数体内this值的能力。
考虑下面的例子:
window.color = 'red';let o = {color: 'blue'};function sayColor() {console.log(this.color);}sayColor(); // redsayColor.call(this); // redsayColor.call(window); // redsayColor.call(o); // blue
这个例子是在之前那个关于this对象的例子基础上修改而成的。同样,sayColor()是一个全局函数,如果在全局作用域中调用它,那么会显示”red”。这是因为this.color会求值为window.color。如果在全局作用域中显式调用sayColor.call(this)或者sayColor.call(window),则同样都会显示”red”。而在使用sayColor.call(o)把函数的执行上下文即this切换为对象o之后,结果就变成了显示”blue”了。使用call()或apply()的好处是可以将任意对象设置为任意函数的作用域,这样对象可以不用关心方法。在前面例子最初的版本中,为切换上下文需要先把sayColor()直接赋值为o的属性,然后再调用。而在这个修改后的版本中,就不需要这一步操作了。ECMAScript 5出于同样的目的定义了一个新方法:bind()。bind()方法会创建一个新的函数实例,其this值会被绑定到传给bind()的对象。比如:
window.color = 'red';let o = {color: 'blue'};function sayColor() {console.log(this.color);}let objectSayColor = sayColor.bind(o);objectSayColor(); // blue
在sayColor()上调用bind()并传入对象o创建了一个新函数objectSayColor()
objectSayColor()中的this值被设置为o,因此直接调用这个函数,即使是在全局作用域中调用,也会返回字符串”blue”
对函数而言,继承的方法toLocaleString()和toString()始终返回函数的代码。返回代码的具体格式因浏览器而异。有的返回源代码,包含注释,而有的只返回代码的内部形式,会删除注释,甚至代码可能被解释器修改过。
由于这些差异,因此不能在重要功能中依赖这些方法返回的值,而只应在调试中使用它们。继承的方法valueOf()返回函数本身。
10.11 函数表达式
定义函数有两种方式:函数声明和函数表达式。
函数声明是这样的:
function functionName(arg0, arg1, agr2) {// 函数体}
函数声明的关键特点是函数声明提升,即函数声明会在代码执行之前获得定义。这意味着函数声明可以出现在调用它的代码之后。因为JavaScript引擎会先读取函数声明,然后再执行代码
第二种创建函数的方式就是函数表达式。
函数表达式有几种不同的形式,最常见的是这样的:
let functionName = function(arg0, arg1, agr2) {// 函数体}
函数表达式看起来就像一个普通的变量定义和赋值,即创建一个函数再把它赋值给一个变量functionName。这样创建的函数叫作匿名函数(anonymous funtion),因为function关键字后面没有标识符。(匿名函数有也时候也被称为兰姆达函数)。未赋值给其他变量的匿名函数的name属性是空字符串。
函数表达式跟JavaScript中的其他表达式一样,需要先赋值再使用。
sayHi();function sayHi() {console.log('Hi!');}// Hi!
sayHi();let sayHi = function() {console.log('Hi!');}// Uncaught ReferenceError: Cannot access 'sayHi' before initialization
理解函数声明与函数表达式之间的区别,关键是理解提升。
比如:
// 千万不要这样做!if (condition) {function sayHi() {console.log('Hi!');}} else {function sayHi() {console.log('Hi!');}}
这段代码看起来很正常,就是如果condition为true,则使用第一个sayHi()定义;否则,就使用第二个。
事实上,这种写法在ECAMScript中不是有效的语法。JavaScript引擎会尝试将其纠正为适当的声明。
问题在于浏览器纠正这个问题的方式并不一致。多数浏览器会忽略condition直接返回第二个声明。Firefox会在condition为true时返回第一个声明。这种写法很危险,不要使用。
不过,如果把上面的函数声明换成函数表达式就没问题了:
// 没问题let sayHi;if (condition) {function sayHi() {console.log('Hi!');}} else {sayHi = function() {console.log('Hi!');}}
10.12 递归
递归函数通常的形式是一个函数通过名称调用自己
如下面的例子所示:
function factorial(num) {if (num <= 1) {return 1;} else {return num * factorial(num - 1);}}
这是经典的递归阶乘函数。虽然这样写是可以的,但如果把这个函数赋值给其他变量,就会出问题:
let anotherFactorial = factorial;factorial = null;console.log(anotherFactorial(4));// Uncaught TypeError: factorial is not a function at factorial
这里把factorial()函数保存在了另一个变量anotherFactorial中,然后将factorial设置为null,于是只保留了一个对原始函数的引用。
而在调用anotherFactorial()时,要递归调用factorial(),但因为它已经不是函数了,所以会出错。
在写递归函数时使用arguments.callee可以避免这个问题。
arguments.callee就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用,
如下所示:
function factorial(num) {if (num <= 1) {return 1;} else {return num * arguments.callee(num - 1);}}
把函数名称替换成arguments.callee,可以确保无论通过什么变量调用这个函数都不会出问题。
因此在编写递归函数时,arguments.callee是引用当前函数的首选。
不过,在严格模式下运行的代码不能访问arguments.callee,因为访问会出错。
此时,可以使用命名函数表达式(named function expression)达到目的。
比如:
const factorial = (function f(num) {if (num <= 1) {return 1;} else {return num * f(num - 1);}})
创建了一个命名函数表达式f(),然后将它赋值给了变量factorial。
即使把函数赋值给另一个变量,函数表达式的名称f也不变,因此递归调用不会有问题。
这个模式在严格模式和非严格模式下都可以使用。
