前言
众所周知,在前端开发领域中,函数是一等公民,由此可见函数的重要性,本文旨在介绍函数中的一些特性与方法,对函数有更好的认知
正文
1.箭头函数
ECMAScript 6 新增了使用胖箭头(=>)语法定义函数表达式的能力。很大程度上,箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方,都可以使用箭头函数:
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
使用箭头函数须知:
- 箭头函数的函数体如果不用大括号括起来会隐式返回这行代码的值
- 箭头函数不能使用
arguments
、super
和new.target
,也不能用作构造函数 - 箭头函数没有
prototype
属性
2.函数声明与函数表达式
JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
// 没问题
console.log(sum(10, 10));
function sum(num1, num2) {
return num1 + num2;
}
以上代码可以正常运行,因为函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升
(function declaration hoisting)。在执行代码时,JavaScript 引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部。因此即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部。如果把前面代码中的函数声明改为等价的函数表达式,那么执行的时候就会出错:
// 会出错
console.log(sum(10, 10));
let sum = function(num1, num2) {
return num1 + num2;
};
上述代码的报错有一些同学可能认为是let导致的暂时性死区
。其实原因并不出在这里,这是因为这个函数定义包含在一个变量初始化语句中,而不是函数声明中。这意味着代码如果没有执行到let
的那一行,那么执行上下文中就没有函数的定义。大家可以自己尝试一下,就算是用var
来定义,也是一样会出错。
3.函数内部
在 ECMAScript 5 中,函数内部存在两个特殊的对象:arguments
和 this
。ECMAScript 6 又新增了 new.target
属性。
arguments
它是一个类数组对象
,包含调用函数时传入的所有参数。这个对象只有以 function 关键字定义函数(相对于使用箭头语法创建函数)时才会有。但 arguments 对象其实还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针。
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
// 上述代码可以运用arguments来进行解耦
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
这个重写之后的 factorial()函数已经用 arguments.callee
代替了之前硬编码的 factorial。这意味着无论函数叫什么名称,都可以引用正确的函数。
arguments.callee 的解耦示例
let trueFactorial = factorial;
factorial = function() {
return 0;
};
console.log(trueFactorial(5)); // 120
console.log(factorial(5)); // 0
这里 factorial 函数在赋值给trueFactorial后被重写了 那么我们如果在递归中不使用arguments.callee
那么显然trueFactorial(5)的运行结果也是0,但是我们解耦之后,新的变量还是可以正常的进行
this
函数内部另一个特殊的对象是 this
,它在标准函数和箭头函数中有不同的行为。
在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在网页的全局上下文中调用函数时,this 指向 windows)。
在箭头函数中,this引用的是定义箭头函数的上下文。
caller
这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为 null。
function outer() {
inner();
}
function inner() {
console.log(inner.caller);
}
outer();
以上代码会显示 outer()函数的源代码。这是因为 ourter()调用了 inner(),inner.caller指向 outer()。如果要降低耦合度,则可以通过 arguments.callee.caller 来引用同样的值:
function outer() {
inner();
}
function inner() {
console.log(arguments.callee.caller);
}
outer();
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(); // Error: King must be instantiated using "new"
这里可以做一些延申,还有没有其他办法来判断函数是否通过new来调用的呢?
可以使用 instanceof
来判断。我们知道在new
的时候发生了哪些操作?用如下代码表示:
var p = new Foo()
// 实际上执行的是
// 伪代码
var o = new Object(); // 或 var o = {}
o.__proto__ = Foo.prototype
Foo.call(o)
return o
上述伪代码在MDN是这么说的:
- 一个继承自 Foo.prototype 的新对象被创建。
- 使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数列表,Foo 不带任何参数调用的情况。
- 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。(一般情况下,构造函数不返回值,但是用户可以选择主动返回对象,来覆盖正常的对象创建步骤)
new
的操作说完了 现在我们看一下 instanceof
,MDN上是这么说的:instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
也就是说,A的N个__proto__ 全等于 B.prototype,那么A instanceof B返回true
,现在知识点已经介绍完毕,可以开始上代码了
function Person() {
if (this instanceof Person) {
console.log("通过new 创建");
return this;
} else {
console.log("函数调用");
}
}
const p = new Person(); // 通过new创建
Person(); // 函数调用
解析:我们知道new构造函数的this指向实例,那么上述代码不难得出以下结论this.__proto__ === Person.prototype
。所以这样就可以判断函数是通过new还是函数调用
这里我们其实还可以将 this instanceof Person
改写为 this instanceof arguments.callee
4.闭包
终于说到了闭包,闭包这玩意真的是面试必问,所以掌握还是很有必要的
闭包
指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
function foo() {
var a = 20;
var b = 30;
function bar() {
return a + b;
}
return bar;
}
上述代码中,由于foo函数内部的bar函数使用了foo函数内部的变量,并且bar函数return把变量return了出去,这样闭包就产生了,这使得我们可以在外部拿到这些变量。
const b = foo();
b() // 50
foo函数在调用的时候创建了一个执行上下文,可以在此上下文中使用a,b变量,理论上说,在foo调用结束,函数内部的变量会v8引擎的垃圾回收机制通过特定的标记回收。但是在这里,由于闭包的产生,a,b变量并不会被回收,这就导致我们在全局上下文(或其他执行上下文)中可以访问到函数内部的变量。
我之前看到了一个说法:
无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包,闭包包含在函数创建时作用域中的所有变量,类似于背包,函数定义附带一个小背包,它的包中存储了函数定义时作用域中的所有变量
以此引申出一个经典面试题
for (var i = 1; i <= 5; i++) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
}
怎样可以使得上述代码的输出变为1,2,3,4,5?
使用es6我们可以很简单的做出解答:将var换成let。
那么我们使用刚刚学到的闭包知识怎么来解答呢?代码如下:
for (var i = 1; i <= 5; i++) {
(function (i) {
setTimeout(function timer() {
console.log(i);
}, i * 1000);
})(i)
}
根据上面的说法,将闭包看成一个背包,背包中包含定义时的变量,每次循环时,将i值保存在一个闭包中,当setTimeout中定义的操作执行时,则访问对应闭包保存的i值,即可解决。
5.立即调用的函数表达式(IIFE)
如下就是立即调用函数表达式
(function() {
// 块级作用域
})();
使用 IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。
// IIFE
(function () {
for (var i = 0; i < count; i++) {
console.log(i);
}
})();
console.log(i); // 抛出错误
ES6的块级作用域:
// 内嵌块级作用域
{
let i;
for (i = 0; i < count; i++) {
console.log(i);
}
}
console.log(i); // 抛出错误
// 循环的块级作用域
for (let i = 0; i < count; i++) {
console.log(i);
}
console.log(i); // 抛出错误
IIFE的另一个作用就是上文中的解决settimeout的输出问题
附录知识点
关于instanceof
Function instanceof Object;//true
Object instanceof Function;//true
上述代码大家可以尝试在浏览器中跑一下,非常的神奇,那么这是什么原因呢?
//构造器Function的构造器是它自身
Function.constructor=== Function;//true
//构造器Object的构造器是Function(由此可知所有构造器的constructor都指向Function)
Object.constructor === Function;//true
//构造器Function的__proto__是一个特殊的匿名函数function() {}
console.log(Function.__proto__);//function() {}
//这个特殊的匿名函数的__proto__指向Object的prototype原型。
Function.__proto__.__proto__ === Object.prototype//true
//Object的__proto__指向Function的prototype,也就是上面中所述的特殊匿名函数
Object.__proto__ === Function.prototype;//true
Function.prototype === Function.__proto__;//true
结论:
- 所有的构造器的constructor都指向Function
- Function的prototype指向一个特殊匿名函数,而这个特殊匿名函数的__proto__指向Object.prototype