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

第 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
    评论
### 回答1: 《JavaScript高级程序设计》第4是一本深入讲解JavaScript编程语言的书籍。该书详细介绍了JavaScript的基础知识、语法、面向对象编程、DOM操作、事件处理、Ajax、JSON等方面的内容。此外,该书还介绍了一些高级技术,如模块化编程、正则表达式、Web Workers、Web Storage等。该书适合有一定JavaScript基础的读者阅读,可以帮助读者深入了解JavaScript编程语言,提高编程技能。 ### 回答2: 《JavaScript高级程序设计》第4(以下简称《JS高级程序设计》)是由李炎恢编写的一部JavaScript语言的经典教材,它被誉为“JavaScript圣经”。本书全面深入地讲解了JavaScript语言的核心概念、高级特性和最佳实践,对于想要深入学习JavaScript的开发者来说是一本必读之作。 首先,本书从JavaScript的基础知识开始,包括JavaScript的数据类型、变量、运算符、数等。随后,本书详细介绍了JavaScript的面向对象编程,包括对象、原型、继承等概念,以及使用构造数和类来创建对象的方法。 其次,本书不仅讲述了JavaScript的基本语法,更详细深入地介绍了诸如数表达式、闭包、高阶数、递归等高级特性,对于想要提高自己的JavaScript编程能力的开发者很有帮助。 最后,本书也介绍了一些实际的开发技巧和最佳实践,例如DOM操作、事件处理、Ajax、JSON、模块化开发等,让开发者在实际的开发中更加得心应手。 总之,《JavaScript高级程序设计》第4是一本权威性的JavaScript经典教材,它涵盖了JavaScript的核心概念、高级特性和最佳实践,对于想要深入了解JavaScript的开发者来说是一本必读之作。无论是初学者还是有经验的开发者,都可以从中找到大量有用的知识和实践经验。 ### 回答3: 《JavaScript高级程序设计》第4是由三位著名的前端开发专家编写的JavaScript权威教程。本书介绍了JavaScript的核心概念、语言特性和应用方法,以及一些高级技巧和最佳实践。 本书的第一部分从JavaScript基础语法开始介绍,包括变量声明、数据类型、操作符、语句和数等方面。第二部分主要介绍JavaScript的面向对象编程,包括原型链、继承和封装等概念。第三部分主要介绍JavaScript的一些高级特性,包括闭包、异步编程、事件和延迟加载等内容。第四部分主要介绍了如何使用JavaScript实现一些实际应用,包括调试、性能优化、动态Web页面和跨域请求等方面。 本书内容全面、深入,不仅介绍了JavaScript的基础知识,更重要的是让读者理解了JavaScript的思想和编程风格。编写本书的三位专家都是行业内的大牛,他们的经验和见解非常宝贵,能够帮助读者更好地理解JavaScript。同时,本书的配套网站还提供了很多实例代码和练习题,读者可以通过这些实践来深入理解JavaScript。 总之,《JavaScript高级程序设计》第4是一本非常不错的JavaScript权威教程,无论是初学者还是专业开发者都可以从中受益匪浅。如果你想深入学习JavaScript,这本书绝对值得一读。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值