JavaScript高级程序设计 第4版 -- 函 数

本文深入探讨JavaScript中的函数,包括函数声明、箭头函数的使用,函数作为值的特性,以及闭包和递归的概念。特别关注箭头函数的限制,如不能使用arguments和作为构造函数,以及函数内部的this和arguments对象。此外,还讨论了尾调用优化及其条件,以及如何利用闭包避免内存泄漏。最后,展示了如何通过立即调用的函数表达式创建私有作用域。
摘要由CSDN通过智能技术生成

第 10 章 函 数

函数创建(以下三个函数等价)

//函数声明
function sum (num1, num2) { 
 return num1 + num2; 
}

//函数表达式
let sum = function(num1, num2) { 
 return num1 + num2; 
};

//匿名函数
let sum = (num1, num2) => { 
 return num1 + num2; 
};

10.1 箭头函数

// 以下两种写法都有效
let double = (x) => { return 2 * x; }; 
let triple = x => { return 3 * x; }; 
// 没有参数需要括号
let getRandom = () => { return Math.random(); }; 
// 多个参数需要括号
let sum = (a, b) => { return a + b; }; 
//只有一条返回语句可省略{}
let triple = (x) => 3 * x;

注意:箭头函数不能使用 arguments、super 和new.target,也不能用作构造函数。

此外,箭头函数也没有 prototype 属性,this指向离他最近的上层非箭头函数的this

10.2 函数名

函数名就是指向函数的指针。使用不带括号的函数名会访问函数指针,而不会执行函数

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

10.3参数

可以在函数内部访问arguments(类数组对象)对象,从中取得传进来的每个参数值。

箭头函数中没有arguments对象,但是可以在包装函数中把它提供给箭头函数

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

10.4 没有重载

在js中函数没有重载,如果定义两个同名函数,则后定义的会覆盖先定义的

function addSomeNumber(num) { 
 return num + 100; 
} 
function addSomeNumber(num) { 
 return num + 200; 
} 
let result = addSomeNumber(100); // 300

10.5 默认参数值

在es5.1之前,实现默认参数的一种常用方式就是检测某个参数是否等于undefined,如果是则意味着没有传这个参数,那就给他一个赋值

function makeKing(name) { 
 name = (typeof name !== 'undefined') ? name : 'Henry'; 
 return `King ${name} VIII`; 
} 
console.log(makeKing()); // 'King Henry VIII' 
console.log(makeKing('Louis')); // 'King Louis VIII'

es6之后支持显式定义默认参数

function makeKing(name = 'Henry') { 
 return `King ${name} VIII`; 
} 
console.log(makeKing('Louis')); // 'King Louis VIII' 
console.log(makeKing()); // 'King Henry VIII'

在使用默认参数时,arguments 对象的值不反映参数的默认值,只反应传给函数的参数

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

10.6 参数扩展与收集

通过arguments

let values = [1, 2, 3, 4]; 
function getSum() { 
 let sum = 0; 
 for (let i = 0; i < arguments.length; ++i) { 
 sum += arguments[i]; 
 } 
 return sum; 
}

通过apply()方法

console.log(getSum.apply(null, values)); // 10

通过扩展运算符

console.log(getSum(...values)); // 10

10.7函数声明与函数表达式

js在加载数据时对它们是有区别的,

js在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义,所以会存在函数声明提升

而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义

// 没问题 
console.log(sum(10, 10)); 
function sum(num1, num2) { 
 return num1 + num2; 
}
// 会出错
console.log(sum(10, 10)); 
let sum = function(num1, num2) { 
 return num1 + num2; 
};

10.8 函数作为值

因为函数名在es中就是变量,所以函数可以用到任何可以使用变量的地方,这就意味着不仅可以把函数作为参数传给另一个函数,还可以在一个函数中返回另一个函数

function callSomeFunction(someFunction, someArgument) { 
 return someFunction(someArgument); 
}

10.9 函数内部

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

10.9.1 arguments

这个对象只有在function关键字定义函数时才会有,此对象有一个callee属性,指向arguments对象所在函数的指针

function factorial(num) { 
 if (num <= 1) { 
 return 1; 
 } else { 
   //这种方法,必须保证函数名是factorial
 return num * factorial(num - 1); 
 } 
}

使用arguments。callee可以让函数逻辑与函数名解耦:

function factorial(num) { 
 if (num <= 1) { 
 return 1; 
 } else { 
   //无论函数叫什么名称,都可以引用正确的函数
 return num * arguments.callee(num - 1); 
 } 
}

10.9.2 this

在标准函数中,this指向Windows

在构造函数中,指向创建出来的实例

在对象的方法里调用,指向调用方法的对象

在严格模式下,this为undefined

在箭头函数中,指向上下文中离他最近的非箭头函数中的this

10.9.3 caller

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

function outer() { 
 inner(); 
} 
function inner() { 
 console.log(inner.caller);   //ƒ outer() { inner(); }
} 
outer();

如果要降低耦合度,则可以通过 arguments.callee.caller 来引用同样的值:

function outer() { 
 inner(); 
} 
function inner() { 
 console.log(arguments.callee.caller); 
} 
outer();

10.9.4 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"

10.10 函数属性和方法

由于函数也是对象,因此有属性和方法

每个函数有2个属性,.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

prototype 是保存引用类型所有实例方法的地方,这意味着 toString()、valueOf()等方法实际上都保存在 prototype 上,进而由所有实例共享。

函数还有2个方法:apply()、call(),可以改变this指向

apply()接收2个参数:函数内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()类似,第一个参数this的值,剩下的参数逐个传递

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

10.11 函数表达式

没有变量提升会报错:

let functionName = function(arg0, arg1, arg2) { 
 // 函数体 
};

10.12 递归

递归函数:一个函数通过名称调用自己

如下阶乘

function factorial(num) { 
 if (num <= 1) { 
 return 1; 
 } else { 
 return num * factorial(num - 1); 
 } 
}

10.13 尾调用优化

即外部函数的返回值是一个内部函数的返回值

ECMAScript 6 规范新增了一项内存管理优化机制,让 JavaScript 引擎在满足条件时可以重用栈帧。

function outerFunction() { 
 return innerFunction(); // 尾调用
}

在 ES6 优化之前,执行这个例子会在内存中发生如下操作。

(1) 执行到 outerFunction 函数体,第一个栈帧被推到栈上。

(2) 执行 outerFunction 函数体,到 return 语句。计算返回值必须先计算 innerFunction。

(3) 执行到 innerFunction 函数体,第二个栈帧被推到栈上。

(4) 执行 innerFunction 函数体,计算其返回值。

(5) 将返回值传回 outerFunction,然后 outerFunction 再返回值。

(6) 将栈帧弹出栈外。

在 ES6 优化之后,执行这个例子会在内存中发生如下操作。

(1) 执行到 outerFunction 函数体,第一个栈帧被推到栈上。

(2) 执行 outerFunction 函数体,到达 return 语句。为求值返回语句,必须先求值 innerFunction。

(3) 引擎发现把第一个栈帧弹出栈外也没问题,因为 innerFunction 的返回值也是 outerFunction

的返回值。

(4) 弹出 outerFunction 的栈帧。

(5) 执行到 innerFunction 函数体,栈帧被推到栈上。

(6) 执行 innerFunction 函数体,计算其返回值。

(7) 将 innerFunction 的栈帧弹出栈外。

10.13.1 尾调用优化的条件

 代码在严格模式下执行;

 外部函数的返回值是对尾调用函数的调用;

 尾调用函数返回后不需要执行额外的逻辑;

 尾调用函数不是引用外部函数作用域中自由变量的闭包。

"use strict"; 
// 有优化:栈帧销毁前执行参数计算
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(); 
}

10.13.2 尾调用优化代码

举个递归的例子:斐波纳契数列的函数

function fib(n) { 
 if (n < 2) { 
 return n; 
 } 
 return fib(n - 1) + fib(n - 2); 
} 
console.log(fib(0)); // 0 
console.log(fib(1)); // 1 
console.log(fib(2)); // 1 
console.log(fib(3)); // 2 
console.log(fib(4)); // 3 
console.log(fib(5)); // 5 
console.log(fib(6)); // 8

显然这个函数不符合尾调用优化的条件,因为返回语句中有一个相加的操作fib(n)的栈帧数的内存复杂度是 O(2n)。

为此可以使用两个嵌套的函数,外部函数作为基础框架,内部函数执行递归:

"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); 
}

10.14 闭包

指引用了而另一个函数作用域中的变量,通常在嵌套函数中

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; 
 } 
 }; 
}

函数内部的代码在访问变量时,就会使用给定的名称从作用域链中查找变量。函数执行完毕后,局部活动对象会被销毁,内存中就只剩下全局作用域。而闭包会保存到内存中,直到函数被销毁后才会被销毁;

10.14.1 内存泄漏

由于 IE 在 IE9 之前对 JScript 对象和 COM 对象使用了不同的垃圾回收机制,所以闭包在这些旧版本 IE 中可能会导致问题。在这些版本的 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,包含函数的活动对象上还是保存着对它的引用。因此,必须再把 element 设置为 null。这样就解除了对这个 COM 对象的引用,其引用计数也会减少,从而确保其内存可以在适当的时候被回收。

10.15 立即调用的函数表达式

(function() { 
 // 块级作用域 
})();
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值