第十章:函数

函数实际上是对象,函数名就是指向函数对象的指针。

函数定义

// 函数声明式定义
function sum(num1, num2) {
    return num1 + num2;	
}
// 函数表达式定义 注意:这里函数末尾是有分号的
let sum = function(num1, num2) {
    return num1 + num2;
};
// 箭头函数 arrow function
let sum = (num1, num2) => {
    return num1 + num2;
};
// 使用function构造函数
// 不推荐,这段代码会被解释两次,第一次将它当作常规的es代码 第二次解释传给构造函数的字符串,影响性能
let sum = new Function("num1", "num2", "return num1 + num2");

箭头函数

任何可以使用函数表达式的地方,都可以使用箭头函数,如果只有一个参数,可以不用括号,多个参数还是要加上括号的。

let triple = x => { return 3 * x; };

箭头函数不能使用 arguments、super 和 new.target,也不能用作构造函数。此外,箭头函数也没有 prototype 属性。

函数名

函数名就是指向函数的指针,所以它们跟其他包含对象指针的变量具有相同的行为。这意味着一个函数可以有多个名称。

function sum(num1, num2) {
    return num1 + num2;
}
let anotherSum = sum;
console.log(anotherSum(10,10)); // 20

使用不带括号的函数名会访问函数指针,而不会执行函数。因此,anotherSum 和 sum 都指向同一个函数。

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); //(空字符串)
console.log((new Function()).name); // anonymous

理解参数

ES函数的参数在内部表现为一个数组,函数被调用时总会接收一个数组,但是函数不关心这个数组中包含什么。

arguments对象是一个类数组对象,它接收传入的参数数组,arguments.length表示参数的长度

function doAdd(num1, num2) { 
 arguments[1] = 10; 
 console.log(arguments[0] + num2); 
} 
// arguments的值会自动同步到对应的命名参数,所以修改arguments[1] 的值也会修改num2的值,因此两者的值都是10,但这并不意味着它们都访问同一个内存地址,它们在内存中还是分开的,只不过会保持同步而已。另外还要记住一点:如果只传了一个参数,然后把 arguments[1]设置为某个值,那么这个值并不会反映到第二个命名参数。这是因为 arguments 对象的长度是根据传入的参数个数,而非定义函数时给出的命名参数个数确定的
doAdd(1,2); // 11

严格模式下,arguments会有一些变化,首先,像前面那样给 arguments[1]赋值不会再影响 num2 的值。就算把 arguments[1]设置为 10,num2 的值仍然还是传入的值。其次,在函数中尝试重写 arguments 对象会导致语法错误。

箭头函数中的参数

如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用 arguments 关键字访问,而只 能通过定义的命名参数访问。

解决:可以将箭头函数放在包装函数中,把它提供给箭头函数

function foo() { 
 let bar = () => { 
 console.log(arguments[0]); // 5 
 }; 
 bar(); 
} 
foo(5); 

没有重载

ES函数不能重载,在其他语言比如 Java 中,一个函数可以有两个定义, 只要签名(接收参数的类型和数量)不同就行。如前所述,ECMAScript 函数没有签名,因为参数是由 包含零个或多个值的数组表示的。

如果在 ECMAScript 中定义了两个同名函数,则后定义的会覆盖先定义的。

默认参数值

ES6支持显示定义默认参数,只要在函数定义中的参数后面用=就可以为参数赋一个默认值。

function makeKing(name = 'Henry') { 
 return `King ${name} VIII`; 
}

在使用默认参数时,arguments对象的值不反映参数的默认值,只反映传给函数的参数。如果未传参,那么反映的就是undefined。

function makeKing(name = 'Henry') { 
 name = 'Louis'; 
 return `King ${arguments[0]}`; 
} 
console.log(makeKing()); // 'King undefined' 
console.log(makeKing('Louis')); // 'King Louis' 

默认参数作用域与暂时性死区

给多个参数定义默认值实际上跟使用let关键字顺序声明变量一样,默认参数会按照定义它们的顺序依次被初始化。因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数。

function makeKing(name = 'Henry', numerals = name) { 
 return `King ${name} ${numerals}`; 
} 

但是参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。

// 调用时不传第一个参数会报错
function makeKing(name = numerals, numerals = 'VIII') { 
 return `King ${name} ${numerals}`; 
}

参数扩展与收集

ES6新增了扩展操作符,它可以用于调用函数传参,也可以用于定义函数参数。

扩展参数

let values = [1,2,3,4];
// 下面的函数功能:将数组中的数值累加,返回和
function getSum() {
    let sum = 0;
    for(let i = 0;i<arguments.length; ++i) &#123;
        sum += arguments[i];
    &#125;
    return sum;
&#125;

// 使用扩展操作符可以这样传递参数
console.log(getSum(...value)); // 10

因为数组的长度已知,所以在使用扩展操作符传参的时候,并不妨碍在其前面或后面再传其他的值, 包括使用扩展操作符传其他参数.

console.log(getSum(-1, ...values)); // 9 
console.log(getSum(...values, 5)); // 15 
console.log(getSum(-1, ...values, 5)); // 14 
console.log(getSum(...values, ...[5,6,7])); // 28 

对函数中的 arguments 对象而言,它并不知道扩展操作符的存在,而是按照调用函数时传入的参数接收每一个值.

let values = [1,2,3,4] 
function countArguments() &#123; 
 console.log(arguments.length); 
&#125; 
countArguments(-1, ...values); // 5 
countArguments(...values, 5); // 5 
countArguments(-1, ...values, 5); // 6 
countArguments(...values, ...[5,6,7]); // 7

收集参数

在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组。收集参数的结果会得到一个 Array 实例。收集参数的前面如果还有命名参数,则只会收集其余的参数,如果没有则会得到空数组,因为收集参数的结果可变,所以只能把它作为最后一个参数。

// 可以
function ignoreFirst(firstValue, ...values) &#123; 
 console.log(values); 
&#125; 

另外,使用收集参数并不影响arguments对象,它仍然反映调用时传给函数的参数。

函数声明与函数表达式

两者之间是有区别的,JavaScript 引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中 生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。

// 没问题 
console.log(sum(10, 10)); 
function sum(num1, num2) &#123; 
 return num1 + num2; 
&#125; 

上述代码能够正常运行,是因为函数声明会在任何代码执行之前先被读取并添加到执行上下文,这个过程叫做函数声明提升。

在执行代码时,js引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部,因此即使函数定义出现在调用它们的代码之后,引擎也会把函数声明提升到顶部。而如果时函数表达式,就会出错,是因为这个函数定义包含在一个变量初始化语句中,而不是函数声明中。 这意味着代码如果没有执行到加粗的那一行,那么执行上下文中就没有函数的定义,所以上面的代码会 出错。

除了函数什么时候真正有定义这个区别之外,这两种语法是等价的。

函数作为值

可以在一个函数中将另一个函数作为参数传入

// 声明callSomeFunction函数,给他传入另一个函数someFunction和另一个函数需要的参数someArgument
function callSomeFunction(someFunction, someArgument) &#123; 
 return someFunction(someArgument); 
&#125;
// 声明一个函数add10 之后将其传递给函数callSomeFunction中
function add10(num) &#123; 
 return num + 10; 
&#125; 
// 调用callSomeFunction函数
let result1 = callSomeFunction(add10, 10); 
console.log(result1); // 20 

这里需要注意:如果是访问函数而不是调用函数,那就必须不带括号,所以传递给callSomeFunction的必须是add10而不是add10()

从一个函数中返回函数也是可以的:进行数组中对象的排序

// 重写排序规则
function createComparisonFunction(propertyName) &#123;
    return function(object1,object2) &#123;
        let value1 = object1[propertyName];
        let value2 = object2[propertyName];
        // 升序排序
        if(value1 < value2) &#123;
            return -1;
        &#125; else if(value1 > value2) &#123;
            return 1;
        &#125; else &#123;
            return 0;
        &#125;
    &#125;;
&#125;

// 定义对象数组
let data = [
    &#123;name: "zouyang", age: 26&#125;,
    &#123;name: "tianmeng", age: 27&#125;
];

// 调用函数开始排序
// 如果按照姓名排序
data.sort(createComparisonFunction("name"));
console.log(data[0].name); // tianmeng

// 如果按照年龄开始排序
data.sot(createComparisonFunction("age"));
console.log(data[0].name); // zouyang

函数内部

函数内部存在两个特殊的对象:arguments 和 this。ECMAScript 6 又新增 了 new.target 属性。

arguments

arguments 对象是一个类数组对象,包含调用函数时传入的所有参数。这 个对象只有以 function 关键字定义函数(相对于使用箭头语法创建函数)时才会有。arguments 对象其实还有一个 callee 属性,是一个指向 arguments 对象所在函数的 指针。也就是说arguments.callee可以指向该函数本身,而可以不通过函数名访问函数,从而达到与函数名解耦的作用。

// 这个函数是递归方法 阶乘函数
function factorial(num) &#123; 
     if (num <= 1) &#123; 
     return 1; 
 &#125; else &#123; 
     // return num * facorial(num - 1); 使用 arguments.callee 就可以让函数逻辑与函数名解耦
     return num * arguments.callee(num - 1); 
 &#125; 
&#125; 

不过在严格模式下运行的代码是不能访问arguments.callee 的,因为访问会出错。此时,可 以使用命名函数表达式(named function expression)达到目的。

const factorial = (function f(num) &#123;
    if(num <= 1) &#123;
        return 1;
    &#125; else &#123;
        return num * f(num - 1);
    &#125;
&#125;);

this

在标准函数中,this 引用的是把函数当成方法调用的上下文对象,这时候通常称其为 this 值(在 网页的全局上下文中调用函数时,this 指向 windows)

window.color = 'red'; 
let o = &#123; 
 color: 'blue' 
&#125;; 
function sayColor() &#123; 
    // 这个 this 到底引用哪个对象必须到函数被调用时才能确定。
     console.log(this.color); 
&#125; 
// 在全局上下文中调用sayColor(),this 指向 window
sayColor(); // 'red' 
// 把 sayColor()赋值给 o
o.sayColor = sayColor; 
// this 会指向 o
o.sayColor(); // 'blue' 

在箭头函数中,this引用的是定义箭头函数的上下文。

window.color = 'red'; 
let o = &#123; 
 color: 'blue' 
&#125;; 
// 与上面不同的是,sayColor函数是通过箭头函数定义的 所以里面的this一直都是指向全局的,也就是window
let sayColor = () => console.log(this.color); 
sayColor(); // 'red' 
o.sayColor = sayColor; 
o.sayColor(); // 'red' 

在事件回调或定时回调中调用某个函数时,this 值指向的并非想要的对象。此时将回调函数写成箭头函数就可以解决问题。这是因为箭头函数中的 this 会保留定义该函数时的上下文

// 在king函数中通过箭头函数的形式定义了一个回调函数setTimeout
function King() &#123; 
 this.royaltyName = 'Henry'; 
 // this 引用 King 的实例
 setTimeout(() => console.log(this.royaltyName), 1000); 
&#125; 
// 在queen函数中通过函数声明的形式定义了一个回调函数setTimeout
function Queen() &#123; 
 this.royaltyName = 'Elizabeth'; 
 // this 引用 window 对象
 setTimeout(function() &#123; console.log(this.royaltyName); &#125;, 1000); 
&#125; 
new King(); // Henry 
new Queen(); // undefined

caller

这个属性引用的是调用当前函数的函数,或者如果是 在全局作用域中调用的则为 null。

function outer() &#123; 
 inner(); 
&#125; 
function inner() &#123; 
 console.log(inner.caller); 
&#125; 
outer();  // 显示outer()函数的源代码,因为调用inner函数的是函数outer

new target

ES5新增了new.target属性,它可以检测函数是否使用new关键字。如果是正常调用的(不使用new),则new.target的值是undefined,如果是new关键字调用的,将引用被调用的构造函数。

function King() &#123; 
 if (!new.target) &#123; 
 throw 'King must be instantiated using "new"' 
 &#125; 
 console.log('King instantiated using "new"'); 
&#125; 
new King(); // King instantiated using "new" 
King(); // Error: King must be instantiated using "new"

函数的属性与方法

每个函数都有两个属性:length和prototype

length:保存函数定义的命名参数的个数

prototype:保存引用类型所有实例方法,例如toString、valueOf等方法都保存在prototype上,进而由所有实例共享。(不可枚举)

每个函数都有三个方法:

apply:接收两个参数(this,参数数组)第二个参数可以是Array的实例,也可以是arguments对象。

function sum(num1, num2) &#123; 
 return num1 + num2; 
&#125; 
function callSum1(num1, num2) &#123; 
 return sum.apply(this, arguments); // 传入 arguments 对象
&#125; 
function callSum2(num1, num2) &#123; 
 return sum.apply(this, [num1, num2]); // 传入数组
&#125; 

call与apply的作用一样,只是传参的形式不同,通过call()向函数传参时,必须将参数一个一个的列出来。

bind()方法会创建一个新的函数实例,其this值会被绑定到传给bind()的对象。

闭包

闭包指的是那些引用了另一个函数作用域中变量函数,通常是在嵌套函数中实现的。

在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链,然后用arguments 和其他命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数作用域链上的第二个对 象。这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止。

函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它 会在代码执行期间始终存在。而函数局部上下文中的叫活动对象,只在函数执行期间存在。

function createComparisonFunction(propertyName) &#123; 
     return function(object1, object2) &#123; 
         let value1 = object1[propertyName]; 
         let value2 = object2[propertyName]; 
         if (value1 < value2) &#123; 
             return -1; 
         &#125; else if (value1 > value2) &#123; 
             return 1; 
         &#125; else &#123; 
             return 0; 
         &#125; 
     &#125;; 
&#125; 
let compare = createComparisonFunction('name'); 
let result = compare(&#123; name: 'Nicholas' &#125;, &#123; name: 'Matt' &#125;); 

作用域其实就是一个包含指针的列表,每个指针分别指向一个变量对象,但是物理上并不会包含相应的对象。

createComparisonFunction()的 活动对象并不能在它执行完毕后销毁,因为匿名函数的作用域链中仍然有对它的引用。在 createComparisonFunction()执行完毕后,其执行上下文的作用域链会销毁,但它的活动对象仍然会保留 在内存中,直到匿名函数被销毁后才会被销毁。

this对象

如果内部函数没有使用箭头函数定义,则 this 对象会在运 行时绑定到执行函数的上下文。如果在全局函数中调用,则 this 在非严格模式下等于 window,在严 格模式下等于 undefined。如果作为某个对象的方法调用,则 this 等于这个对象。匿名函数在这种情 况下不会绑定到某个对象,这就意味着 this 会指向 window,除非在严格模式下 this 是 undefined。

window.identity = 'The Window'; 
let object = &#123; 
     identity: 'My Object', 
     getIdentityFunc() &#123; 
         return function() &#123; 
             return this.identity; 
         &#125;; 
     &#125; 
&#125;; 
// getIdentitiyFunc()()会立即调用返回的函数,从而得到一个字符串,是window,因为每个函数在调用的时候都会自动创建两个特殊变量:this和arguments,内部函数永远不能直接访问外部函数的这两个变量
console.log(object.getIdentityFunc()()); // 'The Window'
// 但是,如果把this保存到闭包可以访问的另一个变量中,则是可以的
let object = &#123; 
     identity: 'My Object', 
     getIdentityFunc() &#123; 
         let that = this; 
         return function() &#123; 
             return that.identity; 
         &#125;; 
     &#125; 
&#125;;
console.log(object.getIdentityFunc()()); // 'My Object' 

在一些特殊情况下,this值可能并不是我们所期待的值。

window.identity = 'The Window'; 
    let object = &#123; 
     identity: 'My Object', 
     getIdentity () &#123; 
         // 这里this.identity就是object.identity
         return this.identity; 
     &#125; 
&#125;; 
// 正常调用
object.getIdentity(); // 'My Object' 
// 与上一行的含义一致
(object.getIdentity)(); // 'My Object' 
// 执行了一次赋值,然后再调用赋值后的结果,因为赋值表达式的值是函数本身,this不再与任何对象绑定,所以返回的是the window
(object.getIdentity = object.getIdentity)(); // 'The Window' 

立即调用的函数表达式

立即调用的匿名函数又被称作立即调用的函数表达式(IIFE,Immediately Invoked Function Expression)。它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组 括号后面的第二组括号会立即调用前面的函数表达式。

(function() &#123;
    // 块级作用域
&#125;) ();

使用IIFE可以模拟块级作用域

私有变量

任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的 变量。私有变量包括函数参数、局部变量,以及函数内部定义的其他函数。

特权方法(privileged method)是能够访问函数私有变量(及私有函数)的公有方法。在对象上有两种方式创建特权方法。第一种是在构造函数中实现。

// 把所有私有变量和私有函数都定义在构造函数中,然后再创建一个能够访问这些私有成员的特权方法
function MyObject() &#123;
    // 私有变量和私有函数
    let privateVariable = 10;
    
    function privateFunction() &#123;
        return false;
    &#125;
    // 特权方法
    this.publicMethod = function() &#123;
        privateVariable++;
        return privateFunction();
    &#125;
&#125;

小结

函数是 JavaScript 编程中最有用也最通用的工具。ECMAScript 6 新增了更加强大的语法特性,从而 让开发者可以更有效地使用函数。

 函数表达式与函数声明是不一样的。函数声明要求写出函数名称,而函数表达式并不需要。没 有名称的函数表达式也被称为匿名函数。

 ES6 新增了类似于函数表达式的箭头函数语法,但两者也有一些重要区别。

 JavaScript 中函数定义与调用时的参数极其灵活。arguments 对象,以及 ES6 新增的扩展操作符, 可以实现函数定义和调用的完全动态化。

 函数内部也暴露了很多对象和引用,涵盖了函数被谁调用、使用什么调用,以及调用时传入了 什么参数等信息。

 JavaScript 引擎可以优化符合尾调用条件的函数,以节省栈空间。

 闭包的作用域链中包含自己的一个变量对象,然后是包含函数的变量对象,直到全局上下文的 变量对象。

 通常,函数作用域及其中的所有变量在函数执行完毕后都会被销毁。

 闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁。

 函数可以在创建之后立即调用,执行其中代码之后却不留下对函数的引用。

 立即调用的函数表达式如果不在包含作用域中将返回值赋给一个变量,则其包含的所有变量都 会被销毁。

 虽然 JavaScript 没有私有对象属性的概念,但可以使用闭包实现公共方法,访问位于包含作用域 中定义的变量。  可以访问私有变量的公共方法叫作特权方法。

 特权方法可以使用构造函数或原型模式通过自定义类型中实现,也可以使用模块模式或模块增 强模式在单例对象上实现。