9. 函数内部
ES5中函数内部 存在两个特殊的对象: arguments 和 this . ES6又新增了new.target 属性.
9.1. arguments
arguments是一个类数组对象,包含调用函数时传入的所有参数. 这个对象只有以function关键字定义函数时 才会有(即function声明函数以及函数表达式时 可以在函数内部使用 arguments对象. 使用箭头函数来定义时 就不能使用)
arguments对象 还有一个callee属性 (callee被呼叫者) 是一个指向arguments对象所在函数的指针. 在递归调用时, 一般会在函数内部使用该函数名(必须是一模一样的) 这样导致了紧密耦合. 使用arguments.callee 就可以让函数逻辑和函数名解耦.
阶乘函数--紧密耦合的递归 factorial函数名称一旦改变 函数内部就会出现问题(因为函数内部是调用factorial )
function factorial(num){
if(num <= 1){
return 1;
} else {
return num*factorial(num-1);
}
}
使用arguments.callee 让函数逻辑和函数名解耦
function factorial(num){
if(num <= 1){
return 1;
} else {
return num*arguments.callee(num-1);
}
}
let trueFactorial = factorial;
console.log( trueFactorial(5) ) //120
9.2. this
在函数内部的另一个特殊的对象是 this,它在标准函数和箭头函数中有不同的行为。
在标准函数中,this 指向的是把函数当成方法调用的上下文对象(执行上下文描述了 代码运行的环境,包括代码在哪儿执行、可以用哪些变量),我们把它称为 this 值。 (在网页的全局上下文中调用函数时,this 指向windows)
来看下面的例子:
window.color = 'red';
let o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'blue
函数sayColor 在定义时 引用了 this对象. 这个this 到底引用哪个对象必须要等到函数被调用时才能确定. 这个值在代码执行的过程中可能会变.
在全局上下文中调用sayColor(), 该函数内部的this指向windows对象. this.color 相当于 windows.color 所以结果输出'red'
而调用 o.sayColor() this会指向o this.color相当于o.color 所以结果输出'blue'.
注意: 函数名只是保存 该函数对象的引用地址也可以说是保存指针. 因此全局定义的sayColor()函数 和 o.sayColor函数是同一个函数.只是函数执行的上下文不同.
在箭头函数中, this指向的是 定义箭头函数的上下文. 箭头函数不会创建自己的this,而是继承其所在上下文的this.
window.color = 'red';
let o = {
color: 'blue'
};
let sayColor = () => console.log(this.color);
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'red'
在上面的代码中 , sayColor是一个箭头函数, 它不会创建自己的this,而是继承 其定义时所在上下文的this 因为箭头函数sayColor是在全局上下文中定义的 所以this绑定的是 windows对象.( 箭头函数只在乎 其定义时所在的上下文的 this指向谁 它就继承下来) 所以无论是在全局上下文中调用 还是利用对象o调用, this.color都相当于 windows.color 打印结果都是 '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);
}
let king1 = new King(); // Henry
new Queen(); // undefined
因为箭头函数不会创建自己的this 而是继承其定义时所在上下文的this. 所以King函数内部的箭头函数 继承King函数上下文的this指向. 又因为构造函数的this 在使用new创建实例时 this 指向新的实例对象. 所以该箭头函数的this 也指向新的实例对象king1 this.royaltyName 相当于 king1.royaltyName 所以打印结果为'Henry'.
在标准函数中 this指向 把函数当成方法调用的执行上下文. 在setTimeout的实现中 它会在内部将回调函数加入到事件队列, 并在计时结束后 调用回调函数. 但是此调用并没有绑定任何对象,所以回调函数的 this默认是全局对象 window. 标准函数中的this.royaltyName 相当于 window.royaltyName 所以打印结果是 undefined.
9.3. caller
ECMAScript 5 也会给函数对象上添加一个属性:caller。
这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为 null。
9.4. new.target
ECMAScript 中的 函数 始终可以作为 构造函数实例化一个新对象,也可以作为普通函数被调用。
ECMAScript 6 新增了检测函数是否使用 new 关键字调用的 new.target 属性。如果函数是正常调用的, 则 new.target 的值是 undefined;如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数.
利用函数内部的 new.target属性 可以实现 特定函数只能使用new 调用的功能.
function King() {
// 如果King函数是正常调用 没有使用new 那么抛出错误.
if (!new.target) {
throw 'King must be instantiated using "new"'
}
console.log('King instantiated using "new"');
}
new King(); // King instantiated using "new"
King(); // Error: King must be instantiated using "new"
在JavaScript中,throw
关键字用于抛出一个异常。当代码执行到 throw
语句时,它会立即停止当前函数的进一步执行,并将控制权传递给最近的异常处理程序(通常是一个 try...catch
语句)。如果没有捕获到异常,程序将终止并抛出错误。
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); // 1
console.log(sum.length); // 2
console.log(sayHi.length); // 0
以上代码定义了 3 个函数,每个函数的命名参数个数都不一样。sayName()函数有 1 个命名参数,
所以其 length 属性为 1。类似地,sum()函数有两个命名参数,所以其 length 属性是 2。而 sayHi()
没有命名参数,其 length 属性为 0。
prototype 属性 是保存引用类型所有实例方法的地方,这意味着 toString()、valueOf()等方法实际上都保存在 prototype 上,进而由所有实例共享。这个属性在自定义类型时特别重要。
function sayHi() {
console.log("hi");
}
console.log( sayHi.length );
console.log( sayHi.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, [num1, num2]); // 传入数组
}
function callSum2(num1, num2) {
return sum.apply(this, arguments); // 传入 arguments 对象
}
console.log(callSum1(10, 10)); // 20
console.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(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue
sayColor()是一个全局函数,如果在全局作用域中调用它,那么会显示"red"。这是因为 this.color 会求值为 window.color。
如果在全局作用域中显式调用 sayColor.call(this)或者 sayColor.call(window),则同样都会显示"red"。
而在使用 sayColor.call(o)把函数的执行上下文即 this 切换为对象 o 之后,this.color 会求值为 o.color 打印结果就变成了"blue"。
使用 call()或 apply()的好处是可以将任意对象设置为任意函数的作用域,这样对象可以不用关心方法。之前如果想要 将对象o 作为函数 sayColor的作用域 需要先把 sayColor()直接赋值为 o 的属性,然后再
调用。而使用函数方法call或apply,就不需要这一步操作了。
ES 5 出于同样的目的定义了一个新方法:bind()。bind()方法会创建一个新的函数实例, 其 this 值会被绑定到传给 bind()的对象。比如:
window.color = 'red';
var 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()返回函数本身。
function sayHi() {
console.log("hi");
}
console.log( sayHi.toLocaleString() ) ;
console.log( sayHi.valueOf() );
打印结果如图所示:
11. 函数表达式
我们知道,定义函数有两种方式:函数声明和函数表达式。
- 函数声明是这样的:
function functionName(arg0, arg1, arg2) {
// 函数体
}
函数声明的关键特点是函数声明提升,即函数声明会在代码执行之前获得定义。
这意味着函数声明可以出现在调用它的代码之后:
sayHi();
function sayHi() {
console.log("Hi!");
}
这个例子不会抛出错误,因为 JavaScript 引擎会先读取函数声明,然后再执行代码。
- 函数表达式。函数表达式有几种不同的形式,最常见的是这样的:
let functionName = function(arg0, arg1, arg2) {
// 函数体
};
8 函数表达式看起来就像一个普通的变量定义和赋值,即创建一个函数再把它赋值给一个变量functionName。这样创建的函数叫作匿名函数(anonymous funtion),因为 function 关键字后面没有标识符。(匿名函数有也时候也被称为兰姆达函数)。未赋值给其他变量的匿名函数的 name 属性是空字符串。
函数表达式跟 JavaScript 中的其他表达式一样,需要先赋值再使用。下面的例子会导致错误:
sayHi(); // Error! function doesn't exist yet
let sayHi = function() {
console.log("Hi!");
};
理解函数声明与函数表达式之间的区别,关键是理解提升.
创建函数并赋值给变量的能力也可以用于在一个函数中把另一个函数当作值返回:
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;
}
};
}
这里的 createComparisonFunction()函数返回一个匿名函数,这个匿名函数要么被赋值给一个变量,要么可以直接调用。但在 createComparisonFunction()内部,那个函数是匿名的。任何时候, 只要函数被当作值来使用,它就是一个函数表达式。
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
这里把 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);
}
}
把函数名称factorial 替换成 arguments.callee,可以确保无论通过什么变量调用这个函数都不会出问题。因此在编写递归函数时,arguments.callee 是引用当前函数的首选。
不过,在严格模式下运行的代码是不能访问 arguments.callee 的,因为访问会出错。此时,可以使用命名函数表达式(named function expression)达到目的。比如:
let factorial = (function f(num) {
if (num <= 1) {
return 1;
} else {
return num * f(num - 1);
}
});
let anotherFactorial = factorial;
factorial = null;
console.log(anotherFactorial(4));
这里创建了一个命名函数表达式 f(),然后将它赋值给了变量 factorial。即使把函数赋值给另
一个变量,函数表达式的名称 f 也不变,因此递归调用不会有问题。这个模式在严格模式和非严格模式下都可以使用。
上面例子中 使用小括号 包裹等号右边定义的函数 目的是 确保小括号内定义的函数不会误认为是声明, 明确表示该函数是一个表达式.允许它作为一个值出现在赋值语句中.
使用小括号的另一个场景是 立即调用函数表达式(IIFE). 小括号内的函数将会被立即执行.
// ()();
(function(){
console.log('This is an IIFE');
})();