《JavaScript高级程序设计(第三版)》学习笔记
关键词:递归、闭包、闭包的作用域、闭包存在的三个问题、闭包的应用、ES6的补充知识
本章节学习路线:
- 闭包closure:
- 闭包的本质:函数嵌套函数,内部函数可以访问外部函数的变量和参数(与作用域有关系)
- 闭包的应用:模仿块级作用域、私有变量的访问
- 闭包的问题(与解决方案):
- 引用对象的问题 - 访问外部变量的最终值
- this指针问题 - 具有全局性,所以内部闭包的this指向的是全局window
- 内存泄露的问题 - IE的坑
- 补充知识:ES6对闭包的填坑大法:箭头函数
- 模拟块级作用域
- 修复this指向
- 联动学习:递归函数(自己调用自己)
1. 回顾知识点
1.1 函数声明与函数表达式
可在JavaScript学习(3) - 聊聊原型链- 1. 变量 3.6 Function章节中了解更为详细的内容
- 函数声明:解析器会率先读取函数声明,添加到执行环境中,故调用时,代码可写在函数声明之前
// 1. 函数声明 - 语法:
function funName (arg0, arg1, arg2) {
// 函数体
}
// 函数声明提升 - 调用语句可以在函数声明之前,因为执行前代码会先读取函数声明
sayHi()
function sayHi() {
alert('Hi')
}
- 函数表达式:解析器无法对函数声明进行提升,率先添加到执行环境,故调用时,必须写在表达式之后
// 函数表达式 - 语法 - 将一个匿名函数赋值给一个变量
// 匿名函数:function没有命名
var funName = function (arg0, arg1, arg2) {
// 函数体
}
// 函数表达式不能进行函数声明提升,调用要在表达式后面
var sayHi = function () {
alert('Hi')
}
sayHi()
- 注意事项:指针引用、没有重载
// 注意事项
// 备注1:指针引用
var sum2 = sum1 // sum1和sum2均指向同一个function内容
// 备注2:用指针解释function没有重载
var add1 = function (num) { return num + 100 }
add1 = function(num) { return num + 200 } //创建第二个函数的时候,实际上覆盖了第一个引用,故没有重载
1.2 递归函数
- 常规阶乘中,会传入本函数名称进行递归调用,实现自己调用自己
// 经典递归样例 - 阶乘
function factorial (num) {
if (num <= 1) {
return 1
}
else {
return num * factorial(num - 1) // 通过传入本函数名,从而进行递归调用
}
}
- 问题:对原函数名称内容进行重写赋值后,会影响递归效果
// 当将上面的function作为函数表达式引用给另一个变量antoherFactorial,使anotherFactorial也指向同一个function
// 再将原有factorial变量赋值为null时,factorial不再是function类型
// 导致anotherFactorial调用时,内部无法执行factorial(),从而报错
var anotherFactorial = factorial
factorial = null // 对原函数进行了覆盖
alert(anotherFactorial(4)) // error! - 相当于num*null,返回false
- 常规解决方法:
arguments.callee()
// 使用callee解决上述问题
function factorial(num) {
if (num <= 1) {
return 1
}
else {
// 使用arguments.callee来代替函数名factorial
// 避免对原函数名变量的覆盖修改从而影响整个function的功能
return num * arguments.callee(num-1)
}
}
- 新的问题:严格模式中不可使用callee(解决方案:命名函数表达式作为传参)
// !!!严格模式中禁止使用callee
// 不使用callee的解决方法:命名函数表达式作为传参
// 创建一个命名为fun()的命名函数表达式,再将其赋值给变量factorial
// 无论如何赋值,函数名fun()依然有效
var factorial = ( function fun (num) {
if (num <= 1) {
return 1
}
else {
return num * fun(num - 1)
}
})()
2. 闭包 closure
2.1 概念与样例
- 核心概念:有权访问另一个函数作用域中的变量的函数
- 常见的创建方式:在一个函数内部创建另一个函数(函数嵌套函数)
- 注意:闭包的名称只是和英文翻译有问题,和package没有任何关系!!!
- 样例:以比较两个对象的值的function为例
function createComparisonFunction (propertyName) {
// 内部return一个匿名函数,该函数可以访问外部作用域变量,所以是闭包
return function (object1, object2) {
// 匿名函数内部,可以获取上层函数createComparisonFunction()作用域中的变量:propertyName
var value1 = object1[propertyName]
var value2 = object2[propertyName]
if (value1 < value2) return -1
else if (value1 > value2) return 1
else return 0
}
}
// 调用方式:
// 1. 获取闭包函数
var compare = createComparisonFunction('name')
// 2. 执行闭包函数
var result = compare(compareObj1, compareObj2)
// 3. 解除对匿名函数的引用(以便释放内存)
compare = null
分析上述作用域链:(堆栈思想)
-
createComparisonFunction()
:- [
作用域链0
] -createComparisonFunction()
局部活动对象,包含arguments
和propertyName
- [ 作用域链1 ] - 全局变量对象,包含
createComparisonFunction
和result
- [
-
内部匿名函数:
- [ 作用域链0 ] - 自身闭包函数的活动对象,包含
arguments
、object1
、object2
- [
作用域链1
]-createComparisonFunction()
局部活动对象,包含arguments
和propertyName
- [ 作用域链2 ] - 全局变量对象,包含
createComparisonFunction
和result
- [ 作用域链0 ] - 自身闭包函数的活动对象,包含
2.2 闭包的问题
2.2.1 第一个问题:引用变量与作用域
闭包中保存的是上一个作用域的整个变量对象,所以只能取得外层函数中任何变量的最终值 1. 直观的样例:for循环中有闭包
function outer() {
var result = new Array()
for (var i = 0; i < 10; i++) {
result[i] = function () {
// 每个闭包return的i,是获取于外层函数的变量i,但是存入的是i的最终值,也就是循环执行结束后的10
return i
}
}
return result
}
// 调用执行
var outerFun = outer()
for (var n = 0; n < 10; n++) {
console.log(outerFun[n]()) // 10, 10, 10, 10, 10, 10, 10, 10, 10, 10
}
/*
* function outer()解析:
* 1. 每个函数都返回10
* 2. 每个闭包函数的作用域链,都保存着outer()的活动对象,所以都引用的是同一个变量i
* 3. outer()返回后,变量i的值为10, 此时每个闭包函数都引用保存变量i的同一个变量对象,所以每个闭包内的i都是10
*
* 另一种解释方法:
* 1. result[]通过循环存入的是十个一模一样的匿名function,且没有执行返回过结果
* 2. 十个function都是闭包,访问的是上层的作用域的活动对象
* 3. 调用执行的时候,会将result中十个function都执行一遍,获得的i是原本的for循环结束后的最终值
*/
如何改善上面的问题:通过再创建另一个闭包函数,使闭包符合预期
本质:result中不能存入function,而是存入function立即执行的结果的值
function outer() {
var result = new Array()
for (var i = 0; i < 10; i++) {
// 关键:定义一个匿名函数,立即执行匿名函数,并将结果传递给数组;
// 匿名函数参数为num,即为最终的函数需要返回的值
// 函数是按值传递,所以传入的复制给了参数num
// result数组中每个函数都有自己的num变量的副本,而不是直接指向同一个变量i的结果
result[i] = function (num) {
// 函数内部,创建并返回一个访问num的闭包
return function() {
return num
}
}(i) // 调用每个匿名函数的时候,传入了变量i,并立即执行传回数组
}
return result
}
2.2.2 第二个问题:匿名函数的this对象
匿名函数的执行环境具有全局性,所以this对象通常指向的是window
var name = "The window" // 全局对象name
var object = {
name: 'inner object', // object对象属性
getName: function() { // object中直接使用闭包,其中的this指向的是全局对象,而非局部对象
return function() {
return this.name
}
},
getName2: function() {
var that = this // 将object的this在此进行转存,方便闭包函数可以访问,则转存后的this是局部对象
return function() {
return that.name
}
}
}
alert(object.getName()) // 'The Window' (非严格模式下)- 因为object中的闭包函数依然指向的是全局window
alert(object.getName2())// 'inner object' - 因为function内部对object的this对象进行了转存,保证闭包中访问的是局部this
2.2.3 第三个问题: 内存泄漏的问题
本质上是IE的bug:
由于IE9之前的版本对JScript对象和COM对象使用不同的垃圾回收机制,
所以导致IE无法回收已经使用完的闭包内的引用变量
function foo () {
var element = document.getElementById('someElement')
// 只要匿名函数存在,element引用数至少为1
element.onclick = function () {
alert(element.id) // 闭包内,一直执行的是外层引用的element,闭包执行完后,element的引用数无法减少,导致无法释放销毁
}
}
解决方案:通过手动释放对象,解除引用,使IE可以进行回收
function foo () {
var element = document.getElementById('someElement')
var id = element.id // 将element的id的副本保存在一个变量中
element.onclick = function() {
alert(id) // 闭包中直接引用的是副本id
}
element = null // 由于闭包中活动对象包含了element,所以在执行完闭包后,应该设置为null,以解除对element的引用
}
2.3 闭包的应用
2.3.1 模仿块级作用域
JavaScript中没有类C语言中的块级作用域,但是可以用匿名函数模仿块级作用域
2.3.1.1 JavaScript没有块级作用域(回顾)
// 样例:
// 在outputNum()函数中,创建了一个outputNum的执行环境,其中包含活动对象三个:count、i、result
// 在for循环执行结束后,变量和result并不会被销毁,而是依然可以在函数outputNum()内部随处访问
// 备注:类C语言中的块级作用域 - 可以在for循环执行结束后将i和result销毁
function outputNum(count) {
for(var i = 0; i < count; i++) {
var result = i
}
console.log(i, result) // for循环执行结束后依然可以访问,因为i和result是function执行环境中的活动对象
}
outputNum(10) // 10 9
2.3.1.2 用匿名函数模仿块级作用域
- 将函数声明包含在一堆圆括号中,表示其实际上为函数表达式,然后紧跟一堆圆括号以立即执行该函数
- 重点:函数表达式、立即执行
// 模仿块级作用域的语法:
(function () {
// 块级作用域
})()
// 注意:错误的表达方法
// 下面function是一个函数声明,而不是一个函数表达式
function () {
// statements
}()
- 样例:
// 样例:先定义一个函数表达式,然后调用执行该函数
var someFunction = function(arg) {
// statements
}
someFunction(10)
2.3.2 私有变量
- 私有变量/公共方法概念参考:java中的private、public、getter()、setter()
- 注意要点:
- JavaScript中没有私有成员概念,所有对象属性都是公有的;
- 任何在函数中定义的变量,都可以认为是私有变量;
- 私有变量包括函数的参数、局部变量和在函数内部定义的其他函数;
- 可以通过特权方法访问私有变量和私有函数 - 类似setter和getter
// 三个私有变量: num1,num2,sum
function add (num1, num2) {
var sum = num1 + num2
return sum
}
2.3.2.1 特权方法 - 访问私有变量和私有函数
function Add (num1, num2) {
// 私有变量
var sum = num1 + num2
// 私有函数
function privateFun(result) {
return result
}
// 特权方法 - 使用函数表达式创建
this.publicFun = function () {
return privateFun(sum)
}
}
// 创建Add实例对象,然后调用特权方法方法
var result = new Add(10, 20)
result.publicFun() // 30
2.3.2.2 静态私有变量 - 获取块级作用域中的变量和函数
- 在模仿块级作用域中创建的变量和函数时私有的,外部不可直接访问获取
- 可以通过在对象的prototype上,创建特权函数,以实现对这类私有变量的获取
- 注意:私有静态变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变量
(function () {
// 私有变量
var name = ''
// 构造函数Person
Person = function (value) {
name = value
}
// 在原型上创建特权方法
Person.prototype.getName = function() {
return name
}
Person.prototype.setName = function(value) {
name = value
}
})()
// 调用 - 私有静态变量是共享的
// 1. 创建person1对象,person1的name为Nicholas
var person1 = new Person('Nicholas')
person1.getName() // Nicholas
// 2. 创建person2对象
var person2 = new Person('Greg')
person2.getName() // Greg
person1.getName() // Greg - 每个实例都使用的是相同的私有变量,而不是自身的实例变量,所以共享了变化
2.3.3 模块模式 module pattern
- 为单例(只有一个实例的对象)创建私有变量和特权方法
- 如果需要创建一个对象,并以某些数据对其进行初始化,同时通过公有方法公开部分变量,则使用模块模式
var application = function() {
// 私有变量
var components = new Array()
// 对私有变量进行初始化 - 通过BaseComponent()构造函数对私有变量components进行初始化
components.push(new BaseComponent())
// 公共方法 - 直接return一个对象,包含匿名函数,匿名函数中就给以返回私有变量的内容
return {
getComponentCount: function () {
return components.length
}
}
}
2.3.4 增强模式 - 模块模式的增强版本
在返回对象之前加入对其增强的代码
// 用增强模式修改上面的例子
var application = function () {
// 私有变量
var components = new Array()
// 初始化
components.push(new BaseComponent())
// 增强 - 创建application的一个局部副本,增加属性等
var app = new BaseComponent() // 限制application对象必须是BaseComponent的实例
app.type = 'component' // 添加公有属性
app.getComponentCount = function () { // 添加公共方法
return components.length
}
return app // 返回这个副本
}
3. 补充:ES6箭头函数
- es6箭头函数可以修复闭包对于this的指向,也可以实现块级作用域
- 原因:es6箭头函数是词法作用域,不绑定this,只会捕获上下文的this值
3.1 模拟块级作用域
// 用箭头函数声明函数表达式,模拟块级作用域
function output(count) {
// 1. 声明函数表达式,表达式中的变量均为私有变量
var func = (num) => {
for(var i = 0; i < num; i++) {
var result = i
}
}
// 2. 执行函数
func(10)
console.log(i, result) // error!
}
3.2 修复闭包中this的指向
// 用箭头函数修复闭包中的this的指向
var name = "The window" // 全局变量
var object = {
name: 'inner object',
// 常规匿名函数
getName: function () {
return function () {
return this.name // this指向全局对象window
}
},
// es6箭头函数
getName2: function () {
var fn = () => {
return this.name // this指向object对象
}
return fn()
}
}
alert(object.getName()()) // The window
alert(object.getName2()) // 'inner object'
3.3 ES6箭头函数的补充要点
- 箭头函数是匿名函数,不能用作构造函数,不能使用new,使用时会抛出错误
- 箭头函数没有prototype属性
- 箭头函数不绑定arguments
- 箭头函数不能在其参数与其箭头之间包含换行符
- 在简洁的主体中,只指定了一个表达式,该表达式为显式返回值。在块体中,必须使用显式return语句
// 只有一个表达式的时候可以直接写 x*x;相当于return x*x
var func = x => x * x;
// 当参数有多个时,则必须使用return语句;
var func = (x, y) => { return x + y; };
4. 小结
4.1 递归
- 定义:函数自身调用(自己调用自己)
- **优点:**可以实现多种循环场景,比常规的写for简单、优雅
- 缺点:
- 消耗内存
- 时间长(复杂度高)
- 在JavaScript中,一旦在递归函数外部,对同名的内部递归函数名称进行了重写覆盖,则会影响实际的递归调用(非严格模式下,用arguments.callee解决)
4.2 闭包
- 定义:函数嵌套函数,内部函数可以访问外部函数的局部变量
- 常见应用:
- 模仿块级作用域(因JavaScript本身没有块级作用域概念)
- 在构造函数内部,创建特权方法,便于外部访问私有变量
- 存在问题:
- 引用变量问题 - 闭包中访问的是外部函数活动对象最终值(因为闭包并没有立即执行获取结果,本质只是函数表达式)
- this指针问题 - 闭包的this往往指向全局作用域(window),如若想指向构造函数内部变量,需要转存一下this
- 内存泄露问题 - IE的bug,将函数中的变量都保存在内存中而无法回收,容易造成内存泄露。需要手动在退出函数的时候清理一下变量(赋值null)
4.3 闭包与递归的相同点/不同点
- 相同点:
- 都是函数
- 都是函数内部调用函数
- 不同点:
- 闭包是调用外部函数的变量和参数;递归是自己调用自己
- 闭包仅调用一次外部;递归是满足递归条件时进行多次循环调用
- 闭包函数由于变量内存问题,更消耗内存
4.4 闭包与匿名函数的区别
- 闭包是函数嵌套函数,内部函数会使用外部函数变量
- 匿名函数并不使用外部作用域的变量
笔记目录:
JavaScript学习(1) - JavaScript历史回顾
JavaScript学习(3)- 聊聊原型链- 2. 对象与原型
JavaScript学习(3)- 聊聊原型链- 3. 原型链与继承
Github: