红宝书-第十章-函数
- 函数实际上是对象,每个函数都是Function类型的实例,而Function也有属性和方法,跟其他引用类型一样。
箭头函数
- 箭头函数虽然语法简洁,但也有很多场合不适用。箭头函数不能使用arguments、super 和new.target,也不能用作构造函数。此外,箭头函数也没有prototype 属性。
函数名
- 函数名就是指向函数的指针,这意味着一个函数可以有多个名称。
- 使用不带括号的函数名会访问函数指针,而不会执行函数。
- ECMAScript 6 的所有函数对象都会暴露一个只读的name 属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称,也会如实显示成空字符串。
- 如果函数是一个获取函数、设置函数,或者使用bind()实例化,那么标识符前面会加上一个前缀。
理解参数
-
ECMAScript 函数的参数跟大多数其他语言不同。ECMAScript 函数既不关心传入的参数个数,也不关心这些参数的数据类型。
-
ECMAScript 函数的参数在内部表现为一个数组。函数被调用时总会接收一个数组,但函数并不关心这个数组中包含什么。
-
arguments对象
-
arguments 对象是一个类数组对象(但不是Array 的实例),因此可以使用中括号语法访问其中的元素(第一个参数是arguments[0],第二个参数是arguments[1])。而要确定传进来多少个参数,可以访问arguments.length 属性。
-
arguments对象的值始终会与对应的命名参数同步。
function doAdd( num1, num2) { arguments[1] = 10 ; console.log(arguments[0] + num2); // num2 的值始终等于arguments[1] }// 但是严格模式下num2不受argument[1]改变的影响
-
严格模式下,arguments 会有一些变化,arguments在严格模式下属于传入的实参对象,并且是不可变的,即不可在函数内部通过arguments来修改实参值。
-
箭头函数中的参数
- 如果函数是使用箭头语法定义,那么传给函数的参数将不能使用arguments关键字访问,而只能通过定义的命名参数访问。(箭头函数中没有arguments对象)
没有重载
-
ECMAScript 函数没有签名,因为参数是由包含零个或多个值的数组表示的。
-
可以通过检查参数的类型和数量,然后分别执行不同的逻辑来模拟函数重载。
默认参数值
-
只要在函数定义中的参数后面用=就可以为参数赋一个默认值
function makeKing(name = 'defaultName') { return `King ${name} `; }
- 给参数传undefined相当于没有传值,仍然使用默认参数。
-
默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值。
-
函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。而且,计算默认值的函数只有在调用函数但未传相应参数时才会被调用。
默认参数作用域与临时性死区
-
因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数。
function makeKing(name = 'Henry', numerals = naem) { return `King ${name} ${numerals}`; }
-
参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。
参数扩展与收集
-
扩展参数
-
let values = [1,2,3,4]; console.log(getSum(...values));
-
对函数中的arguments 对象而言,它并不知道扩展操作符的存在,而是按照调函数时传入的参数接收每一个值。
-
-
收集参数
- 收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数。
函数声明与函数表达式
- JavaScript 引擎在加载数据时对它们是区别对待的。JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
- 函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升(function declaration hoisting)
函数作为值
-
函数名在ECMAScript中就是变量,所以函数可以用在任何可以使用变量的地方。这意味着不仅可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数。
-
//函数作为参数及返回值。 function callSomeFunction(someFunction, someArgument) { return someFunction(someArgument); }
-
如果是访问函数而不是调用函数,那就必须不带括号。
-
从一个函数中返回另一个函数,例子:一个包含对象的数组,需要按照任意对象属性对数组进行排序。
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 data = { {name: "Zachary", age: 28}, {name: "Nicholas", age: 29} }; data.sort(createComparisonFunction("name")); data.sort(createComparisonFunction("age"));
函数内部
-
ES5中,函数内部有两个特殊对象:arguments和this,ES6有新增了new.target属性。
-
arguments
-
arguments对象还有一个callee属性,是一个指向arguments对象所在函数的指针。
-
使用arguments.callee可以让函数逻辑与函数名解耦。
-
// 使用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)); //120
```
-
this
-
在标准函数和箭头函数中有不同的行为。
-
在标准函数中,this引用的是把函数当成方法调用的上下文对象。到底引用哪个对象必须到this函数被调用时才能确定。
-
在箭头函数中,this引用的是定义箭头函数的上下文。
-
在事件回调或定时回调中调用某个函数时,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(); //Henry new Queen(); //undefined 在全局上下文调用this指向window
-
-
caller
-
这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为null。
function outer() { inner(); } function outer() { console.log(inner.caller); //或者 console.log(arguments.callee.caller); //降低耦合度 }
-
-
new.target
-
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();
-
函数属性与方法
-
ECMAScript 中的函数是对象,因此有属性和方法。每个函数都有两个属性:length和prototype。
-
length 属性保存函数定义的命名参数的个数。
-
prototype 属性是保存引用类型所有实例方法的地方。比如toString()、valueOf()等,进而由所有实例共享。
-
函数有两个方法:apply()和call()。这两个方法都会以指定的this 值来调用函数,即会设置调用函数时函数体内this 对象的值。
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)); //20 console.log(callSum2(10,10)); //20
- call()方法与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
-
新方法:bind()。bind()方法会创建一个新的函数实例,其this值会被绑定到传给bind()的对象。
window.color = 'red'; var o = { color: 'blue' }; function sayColor() { console.log(this.color); } // objetSayColor()中的this值被设置为o let objectSayColor = sayColor.bind(o); objectSayColor(); //blue
-
对函数而言,继承的方法toLocaleString()和toString()始终返回函数的代码。返回代码的
具体格式因浏览器而异。
函数表达式
-
函数表达式不会“提升”。
//没问题, 如果用函数声明则有问题 let sayHi; if(condition) { sayHi = function() { console.log("Hi!"); }; } else { sayHi = function() { console.log("Yo!"); }; }
-
任何时候,只要函数被当作值来使用,它就是一个函数表达式。
递归
-
另一个解决函数名变化的方法:使用命名函数表达式(因为在严格模式下运行的代码是不能访arguments.callee)
//使用命名函数表达式 const factorial = (function f(num) { if(num <= 1) { return 1; } else { return num * f(num - 1); } });
尾调用优化
-
“尾调用”,即外部函数的返回值是一个内部函数的返回值。
function outerFunction() { return innerFunction(); //尾调用 }
-
ES6优化后,把原来需要等待innerFunction计算返回后才能弹出outerFunction()的栈帧,优化为到达return语句时就将outerFunction弹出栈帧。
-
尾调用优化条件:
- 代码在严格模式下执行;
- 外部函数的返回值是对尾调用函数的调用;
- 尾调用函数返回后不需要执行额外的逻辑;
- 尾调用函数不是引用外部函数作用域中自由变量的闭包。
"use strict"; //无优化:尾调用没有返回 function outerFunction() { innerFunction(); } //无忧化:尾调用没有直接返回 function outerFunction() { let innerFunctionResult = innerFunction(); return innerFunctionResult; } //无优化:尾调用返回后必须转型为字符串 function outerFunction() { return innerFunction().toString(); } //无忧化:尾调用是一个闭包 function outerFunction() { let foo = 'bar'; function innerFunction() { return foo; } return innerFunction(); } // 有优化:栈帧销毁前执行参数计算 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(); }
- 之所以要求严格模式,主要因为在非严格模式下函数调用中允许使用f.arguments和f.caller,而它们都会引用外部函数的栈帧。
-
优化实例:
//无优化:返回语句有相加操作 function fib(n) { if(n < 2) { return n; } return fib(n-1) + fib(n-2); } "use strict"; //基础框架 function fib(n) { return fibImpl(0, 1, n); } //执行递归 function fibImpl(a, b, n) { if(n === 0) { return a; } return fibImpl(b, a+b, n-1); } //重构后,调用fib(1000)就不会对浏览器造成威胁。
闭包
-
匿名函数经常被人误认为是闭包(closure)。闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
function createComparisonFunction(propertyName) { return function (object1, object2) { // 该函数引用了外部函数的变量propertyName // 这是因为内部函数的作用域链包含createComparisonFunction()函数的作用域 let value1 = object1[propertyName]; let value2 = object2[propertyName]; if(value1 < value2) { return -1; } else if (value1 > value2) { return 1; } else { return 0; } }; }
-
作用域链实际上就是一个包含指针的列表,每个指针分别指向一个变量对象,但物理上并不会包含相应的对象。
-
在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。然后用arguments和其他命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数作用域链上的第二个对象。这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止。
let compare = createComparisonFunction('name'); let result = compare({name: 'Nicholas'}, {name: 'Matt'});
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JXUzSxxq-1607071215586)(红宝书-第十章-函数/AcroRd32_XMMAeGfWbg.png)]
-
createComparisonFunction()的活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用。createComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留在内存中,直到匿名函数被销毁后才会被销毁。
//需要手动解除对函数的引用 let compareNames = createComparisonFunction('name'); //创建比较函数 let result = compareNames({name: 'Nicholas'}, {name: 'Matt'}); //调用函数 compareNames = null; //解除对函数的引用,这样就可以释放内存
闭包会保留它们包含函数的作用域,所以比其他函数更占用内存。
-
this对象
-
在闭包中使用this会让代码变复杂,如果内部函数没有使用箭头函数定义,则this对象会在运行时绑定到执行函数的上下文。
-
每个函数在被调用时都会自动创建两个特殊变量:this和arguments。内部函数永远不可能直接访问外部函数的这两个变量。但是,如果把this保存到闭包可以访问的另一变量中,则可以实现外部对其的访问。
window.identity = 'The Window'; let object = { identity: 'My Object', getIdentityFunc() { let that = this; //将外部函数的this保存到变量that中。 return function() { return that.identity; }; } } console.log( object.getIdentittyFunc()); //'My Object'
-
this和arguments都是不能直接在内部函数中访问的。如果想访问包含作用域中的arguments对象,则同样需要将其引用先保存到闭包能访问的另一个变量中。
-
-
内存泄漏
-
在旧版本IE中,把HTML元素保存在某个闭包的作用域中,就相当于宣布该元素不能被消耗。例子如下:
-
function assignHandler() { let element = document.getElementById('someElement'); element.onclick = () => console.log(element.id); }
以上代码创建了一个闭包,即element元素的事件处理程序,而这个处理程序又创建了一个循环引用。匿名函数引用着assignHandler()的活动对象,阻止了对element的引用计数归零。只要这个匿名函数存在,element的引用计数就至少等于1。即内存不会被回收。
function assignHandler() { let element = document.getElementById('someElement'); let id = element.id; //消除循环引用 element.onclick = () => console.log(id); element = null; //解除对这个对象的引用 }
将代码简单修改后就可以避免这种情况,闭包改为引用一个保存着element.id 的变量id,从而消除了循环引用。此时闭包还是会引用包含函数的活动对象,而其中包含element,必须再把element设置为null,才解除了对这个对象的引用。
-
立即调用的函数表达式
-
立即调用的匿名函数又被称作立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)。
-
使用IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。CMAScript 5 尚未支持块级作用域,使用IIFE模拟块级作用域是相当普遍的。
// IIFE (function() { for (var i = 0 ; i<count ; i++) { console.log(i); } }) (); console.log(i);
// 解决var关键字声明提升的问题 let divs = document.querySelectorAll('div'); for(var i = 0 ; i<divs.length; i++) { divs[i].addEventListener('click',(function(frozenCounter) { return function() { console.log(frozenCOunter); }; })(i) ); //立即将i传入函数 }
私有变量
-
严格来讲,JavaScript 没有私有成员的概念,所有对象属性都公有的。但是有私有变量的概念,任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量。
-
特权方法(privileged method)是能够访问函数私有变量(及私有函数)的公有方法。
//在构造函数中实现特权方法 function MyObject() { //私有变量和私有函数 let privateVariable = 10; function privateFunction() { return false; } //特权方法 this.publicMethod = function() { privateVariable++; return privateFunction(); }; }
定义在构造函数中的特权方法其实是一个闭包,它具有访问构造函数中定义的所有变量和函数的能力。变量
privateVariable
和函数privateFunction()
只能通过publicMethod()
方法来访问。 -
静态私有变量
-
特权方法也可以通过使用私有作用域定义私有变量和函数来实现。
(function() { //私有变量和私有函数 let privateVariable = 10; function privateFunction() { return false; } //构造函数 MyObject = function() {}; //公有和特权方法 MyObject.prototype.publicMethod = function() { privateVariable++; return privateFUnction(); } })();
这里声明MyObject 并没有使用任何关键字。因为不使用关键字声明的变量会创建在全局作用域中,所以MyObject 变成了全局变量,可以在这个私有作用域外部被访问。
-
这个模式的特点是:私有变量和私有函数是由实例共享的。因为特权方法定义在原型上,所以同样是由实例共享的。特权方法作为一个闭包,始终引用着包含它的作用域。
-
创建静态私有变量可以利用原型更好地重用代码,只是每个实例没有了自己的私有变量。
注意 使用闭包和私有变量会导致作用域链变长,作用域链越长,则查找变量所需的时间也越多。
-
-
模块模式
-
模块模式是在单例对象基础上加以扩展,使其通过作用域链来关联私有变量和特权方法。
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 application = function() { //私有变量和私有函数 let components = new Array(); //初始化 components.push(new BaseComponent()); //创建局部变量保存实例 let app = new BaseComponent(); //公共接口 app.getComponentCount = function() { return components.length; }; app.registerComponent = function(component) { if(typeof component === "object") { components.push(component); } }; //返回实例 return app; }();
在给局部变量app 添加了能够访问私有变量的公共方法之后,匿名函数返回了这个对象。
-