JS高级函数

JS高级函数

JS高级函数

原型和原型链

原型[prototype]

函数的prototype属性

  • 每个函数都有一个prototype属性,它默认指向一个Object空对象(即为:原型对象)
  • 原型对象中有一个属性constructor指向函数对象

image-20210714201049312

给原型对象添加属性

  • 作用:函数的所有实例对象都自动拥有原型中的属性(方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象)
console.log(Date.prototype, typeof Date.prototype)
function Fun() { }
console.log(Fun.prototype) // 默认指向一个Object空对象(没有自定义的属性)

// 原型对象中有一个属性constructor, 它指向函数对象
console.log(Date.prototype.constructor === Date) //true
console.log(Fun.prototype.constructor === Fun) //true

//给原型对象添加属性(一般是方法) ===>实例对象可以访问
Fun.prototype.test = function () { console.log('test()') }
var fun = new Fun()
fun.test()

显式原型与隐式原型

  1. 每个函数function都有一个prototype,即显式原型(属性)
  2. 每个实例对象都有一个[__ proto __],可称为隐式原型(属性)
  3. 对象的隐式原型的值为其对应构造函数的显式原型的值
  4. 内存结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//定义构造函数
function Fn() {
// 内部默认执行语句: this.prototype = {}
}
// 1. 每个函数function都有一个prototype,即显式原型属性, 默认指向一个空的Object对象
console.log(Fn.prototype)
// 2. 每个实例对象都有一个__proto__,可称为隐式原型
//创建实例对象
var fn = new Fn() // 内部默认执行语句: this.__proto__ = Fn.prototype
console.log(fn.__proto__)
// 3. 对象的隐式原型的值为其对应构造函数的显式原型的值
console.log(Fn.prototype===fn.__proto__) // true
//给原型添加方法
Fn.prototype.test = function () {
console.log('test()')
}
//通过实例调用原型的方法
fn.test()

总结:

  • 函数的[prototype]属性: 在定义函数时自动添加的, 默认值是一个空Object对象
  • 对象的[__ proto __]属性: 创建对象时自动添加的, 默认值为构造函数的prototype属性值
  • 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)

image-20240721173151237

原型链

原型链概念

访问一个对象的属性时,

  • 先在自身属性中查找,找到返回
  • 如果没有, 再沿着[__ proto __]这条链向上查找, 找到返回
  • 如果最终没找到, 返回undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Fn(){
this.test1 = function(){
console.log('this is test1')
}
}
Fn.prototype.test2 = function(){}

var fn = new Fn()
fn.test3 = function(){
console.log('this is test3')
}

console.log(fn.__proto__) //{test2: ƒ}
console.log(Fn.prototype) //{test2: ƒ}
console.log(fn) //Fn {test1: ƒ, test3: ƒ}

原型链内存

  • 别名: 隐式原型链
  • 作用: 查找对象的属性(方法)

Object的原型对象的_porto_指向的是null

构造函数/原型/实例对象之间的关系

1
2
var o1 = new Object();
var o2 = {};

对象

1
function Foo(){  }

构造函数原型链

1
2
3
4
5
6
7
8
9
10
// 1.函数的显式原型指向的对象默认是空Object实例对象(Object不满足)
console.log(Fn.prototype instanceof Object) //true
console.log(Object.prototype instanceof Object) //false 指向是null
console.log(Function.prototype instanceof Object) //true Function是Object实例对象

// 2.所有函数都是Function的实例(包括Function本身)
console.log(Function.__proto__ === Function.prototype) //true

// 3.Object的原型对象是原型链的尽头
console.log(Object.prototype.__proto__) //null

属性问题

  • 读取对象的属性值时: 会自动到原型链中查找
  • 设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
  • 方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上(一般不同实例属性不同方法相同)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Fn() { }
Fn.prototype.a = 'xxx'
var fn1 = new Fn()
console.log(fn1.a, fn1) //xxx Fn{}

var fn2 = new Fn()
fn2.a = 'yyy'
console.log(fn1.a, fn2.a, fn2) //xxx yyy Fn{a: "yyy"}

function Person(name, age) {
this.name = name
this.age = age
}
Person.prototype.setName = function (name) {
this.name = name
}
var p1 = new Person('Tom', 12)
p1.setName('Bob')
console.log(p1) //Person {name: "Bob", age: 12}

var p2 = new Person('Jack', 12)
p2.setName('Cat')
console.log(p2) //Person {name: "Cat", age: 12}
console.log(p1.__proto__===p2.__proto__) // true -->所以方法一般定义在原型中

instanceof

  1. instanceof是如何判断的?
  • 表达式: A instanceof B
  • 如果B函数的显式原型对象在A对象的原型链上, 返回true, 否则返回false
  1. Function是通过new自己产生的实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/*
案例1
*/
function Foo() { }
var f1 = new Foo()
console.log(f1 instanceof Foo) // true
console.log(f1 instanceof Object) // true

/*
案例2
*/
console.log(Object instanceof Function) // true
console.log(Object instanceof Object) // true
console.log(Function instanceof Function) // true
console.log(Function instanceof Object) // true

function Foo() {}
console.log(Object instanceof Foo) // false

面试题

题目1:

1
2
3
4
5
6
function A () {}
A.prototype.n = 1
let b = new A()
A.prototype = { n: 2, m: 3}
let c = new A()
console.log(b.n, b.m, c.n, c.m) // 1 undefined 2 3

赋值内存图

题目二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function F (){}
Object.prototype.a = function(){
console.log('a()')
}
Function.prototype.b = function(){
console.log('b()')
}

let f = new F()
f.a() //a()
f.b() //f.b is not a function -->找不到
F.a() //a()
F.b() //b()

console.log(f)
console.log(Object.prototype)
console.log(Function.prototype)

构造函数原型链

执行上下文和执行上下文栈

变量提升和函数提升

  1. 变量声明提升
  • 通过var定义(声明)的变量, 在定义语句之前就可以访问到
  • 值: undefined
  1. 函数声明提升
  • 通过function声明的函数, 在之前就可以直接调用
  • 值: 函数定义(对象)

变量和函数的提升是把变量或者函数提升到其作用域的最前面

  1. 引出一个问题: 变量提升和函数提升是如何产生的?
1
2
3
4
5
6
7
8
9
10
11
12
13
 var a = 3
function fn () {
console.log(a)
var a = 4 //变量提升
}
fn() //undefined
'--------------------------------------------'
console.log(b) //undefined 变量提升
fn2() //可调用 函数提升
// fn3() //不能 fn3的定义方式首先是变量提升,不是函数提升
var b = 3
function fn2() { console.log('fn2()') }
var fn3 = function () { console.log('fn3()') }

执行上下文

  1. 代码分类(按照代码位置划分)
  • 全局代码
  • 函数(局部)代码
  1. 全局执行上下文

    1. 在执行全局代码前将window确定为全局执行上下文

    2. 对全局数据进行预处理

      • var定义的全局变量==>undefined, 添加为window的属性

      • function声明的全局函数==>赋值(fun), 添加为window的方法

      • this==>赋值(window)

    3. 开始执行全局代码

  2. 函数执行上下文

    1. 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)

    2. 对局部数据进行预处理

      • 形参变量==>赋值(实参)==>添加为执行上下文的属性

      • arguments==>赋值(实参列表), 添加为执行上下文的属性

      • var定义的局部变量==>undefined, 添加为执行上下文的属性

      • function声明的函数 ==>赋值(fun), 添加为执行上下文的方法

      • this==>赋值(调用函数的对象)

    3. 开始执行函数体代码

执行上下文栈

  1. 在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象

  2. 在全局执行上下文(window)确定后, 将其添加到栈中(压栈)–>所以栈底百分百是[window]

  3. 在函数执行上下文创建后, 将其添加到栈中(压栈)

  4. 在当前函数执行完后,将栈顶的对象移除(出栈)

  5. 当所有的代码执行完后, 栈中只剩下window

  6. 上下文栈数==函数调用数+1

  7. `

1
2
3
4
5
6
7
8
9
10
11
//1. 进入全局执行上下文
var a = 10
var bar = function (x) {
var b = 5
foo(x + b) //3. 进入foo执行上下文
}
var foo = function (y) {
var c = 5
console.log(a + c + y)
}
bar(10) //2. 进入bar函数执行上下文

执行上下文栈

面试题

函数提升优先级高于变量提升,且不会被变量声明覆盖,但是会被变量赋值覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
测试题1: 先执行变量提升, 再执行函数提升,

*/
function a() {}
var a
console.log(typeof a) // 'function' 函数的优先级高于变量,不会被变量声明覆盖


/*
测试题2:
*/
if (!(b in window)) {
var b = 1
}
console.log(b) // undefined

/*
测试题3:
*/
var c = 1
function c(c) {
console.log(c)
var c = 3 //与此行无关
}
//变量和函数提升后相当于把变量赋值 c=1放在此处,函数声明被变量赋值覆盖
c(2) // 报错 c is not a function

作用域与作用域链

作用域

  1. 理解
    • 一串代码所在的区域
    • 作用域是静态的(相对于上下文对象),在代码编写时候就已经确定
  2. 分类
    • 全局作用域
    • 函数作用域
    • 没有块作用域(ES6 中:作用域块
  3. 作用
    • 隔离变量,不用作用域下同名变量不会有冲突
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*  //没块作用域
if(true) { var c = 3 }
console.log(c)
*/
var a = 10,
b = 20
function fn(x) {
var a = 100, c = 300;
console.log('fn()', a, b, c, x) //100 20 300 10
function bar(x) {
var a = 1000, d = 400
console.log('bar()', a, b, c, d, x)
}
bar(100)//1000 20 300 400 100
bar(200)//1000 20 300 400 200
}
fn(10)

作用域与执行上下文的区别与联系

  1. 区别1: 创建的时间
    • 全局作用域之外,每个函数都会创建自己的作用域作用域在函数定义时就已经确定了,而不是在函数调用时
    • 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
    • 函数执行上下文是在调用函数时, 函数体代码执行之前创建
  2. 区别2: 是否动态的变化
    • 作用域是静态的, 只要函数定义好了就一直存在, 且不会再变化
    • 执行上下文是动态的, 调用函数时创建, 函数调用结束时就会自动释放
  3. 联系:
    • 执行上下文(对象)是从属于所在的作用域
    • 全局上下文环境==>全局作用域
    • 函数上下文环境==>对应的函数使用域

作用域和执行上下文

作用域链

  1. 理解
    • 多个上下级关系的作用域形成的链,方向是从内到外
    • 查找变量的时候就是顺着作用域链来的
  2. 查找一个变量的原则
    • 在当前作用域下的执行上下文中查找对应的属性, 如果有直接返回, 否则进入2
    • 在上一级作用域的执行上下文中查找对应的属性, 如果有直接返回, 否则进入3
    • 再次执行2的相同操作, 直到全局作用域, 如果还找不到就抛出找不到的异常

面试题

作用域在函数定义时候已经确定了,而不是在函数调用时候:

1
2
3
4
5
6
7
8
9
10
var x = 10;
function fn() {
console.log(x);
}

function show(f) {
var x = 20;
f();
}
show(fn); //输出10

静态的作用域

在这个JavaScript代码中,有一个全局变量 x 被赋值为 10,然后定义了两个函数 fnshowfn 函数简单地打印变量 x 的值,而 show 函数接收一个函数 f 作为参数,并在其内部定义了一个局部变量 x(值为 20),然后调用传入的函数 f

show(fn) 被调用时,尽管 show 函数内部有一个局部变量 x(值为 20),但 fn 函数在调用时并没有访问这个局部变量。相反,fn 函数访问的是全局作用域中的 x 变量,因为在其定义的作用域中没有找到名为 x 的局部变量。

因此,这段代码的输出将是 10,这是全局变量 x 的值。

总结一下,JavaScript 中的变量查找遵循所谓的“词法作用域”(也称为静态作用域或声明时作用域)。这意味着函数会记住它们是在哪里被定义的,并且当它们被调用时,会基于这个定义位置来查找变量。在这个例子中,fn 函数记住它是在全局作用域中定义的,因此在执行时它会查找并打印全局作用域中的 x 变量的值。

对象变量不能产生局部作用域:

1
2
3
4
5
6
7
8
9
10
11
12
13
var fn = function () {
console.log(fn)
}
fn()

var obj = {

fn2: function () {
console.log(fn2)
//console.log(this.fn2)
}
}
obj.fn2()

执行 obj.fn2() 时,会遇到一个 ReferenceError,因为 fn2fn2 函数的作用域中并没有被声明或定义为一个局部变量。在 JavaScript 中,函数体内部的作用域会首先查找局部变量,如果没有找到,则会继续向外层作用域查找,直到全局作用域。但是,在这个例子中,fn2 是作为 obj 对象的一个属性(方法)存在的,而不是作为任何函数内部的局部变量或全局变量。

当在 fn2 函数内部尝试访问 fn2 时,JavaScript 会认为您是在尝试访问一个名为 fn2 的局部变量,但实际上并没有这样的局部变量存在。因此,它会继续向外层作用域查找,但同样找不到,因为 fn2 不是外层作用域(在这个例子中是全局作用域)的一部分。

闭包知识预备

例子分析执行上下文

1
2
3
4
5
6
7
1: let a = 3
2: function addTwo(x) {
3: let ret = x + 2
4: return ret
5: }
6: let b = addTwo(a)
7: console.log(b)

上述代码在js引擎里具体工作流程:

  1. 第一行,全局上下文声明了一个新变量a,赋值为3
  2. 第2行到第5行:
    1. 在全局执行上下文中声明了一个addTwo的新变量,给它分配了一个函数定义
    2. 两个{}内的所有内容分配个addTwo,函数内部的代码没有被求值,没有执行,只是储存在一个变量中以备将来使用.
  3. 在第6行:
    1. 在全局执行上下文中声明一个新变量,将其标记为b,变量一声明值为undefined
    2. 遇到赋值操作符,准备给b赋值,看到一个函数被调用(每个函数都会返回一些东西:值,对象,undefined)无论函数返回什么,都将其赋值给b
    3. 调用标记为addTwo的函数,JS将在全局执行上下文内存中查找addTwo,返现它在第2到第5行中定义,包含一个函数定义
    4. 变量a作为参数传递给函数,JS在全局执行上下文中搜索变量a,找到发现值为3,将3作为参数传递给函数,准备执行函数
    5. 执行上下文切换,创建一个本地执行上下文addTwo执行上下文,执行上下文被推送到调用堆栈上,开始函数执行:
      1. 值3是作为一个参数传递的,第一步要在addTwo执行上下文中创建一个变量x,将3赋值给x
      2. 执行第三行,声明一个新变量ret,设置为undefined
      3. 执行相加操作,得到的值赋值给ret
      4. 执行第四行,返回ret内容,函数执行完毕
    6. addTwo 执行上下文被销毁,变量xret被消去了,它们已经不存在了。addTwo 执行上下文从调用堆栈中弹出,返回值返回给调用上下文,在这种情况下,调用上下文是全局执行上下文,因为函数addTwo 是从全局执行上下文调用的。第6行执行完毕
  4. 第 7 行,b的值 5 被打印到控制台了。

例子分析词法作用域

上文中:一个函数可以访问在它的调用上下文中定义的变量,这个就是词法作用域(Lexical scope)****

返回函数的函数[高阶函数]

1
2
3
4
5
6
7
8
9
10
11
1: let val = 7
2: function createAdder() {
3: function addNumbers(a, b) {
4: let ret = a + b
5: return ret
6: }
7: return addNumbers
8: }
9: let adder = createAdder()
10: let sum = adder(val, 8)
11: console.log('example of function returning a function: ', sum)

闭包

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

引出闭包

错误场景:

需求: 点击某个按钮, 提示"点击的是第n个按钮"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>
<!--
需求: 点击某个按钮, 提示"点击的是第n个按钮"
-->
<script type="text/javascript">
var btns = document.getElementsByTagName('button')
//注意[btns]不是一个数组,它是一个伪数组
//每次获取[btns.length]其实都是需要进行计算的(因为它是伪数组)
//所以为了性能更好,在此处赋值,就只需要计算一次
for (var i = 0,length=btns.length; i < length; i++) {
var btn = btns[i]
btn.onclick = function () { //遍历加监听
alert('第'+(i+1)+'个') //结果 全是[4]
}
}
</script>

上述代码实际是给每个button添加一个点击事件,当点击事件实际执行的时候,循环已经结束,执行循环时候引用的全局变量[i]值已经变为3

解决的方法是,给每个button添加属性存储在循环添加事件时候的i的值,让其记住当时的i

利用闭包解决:

​ 利用闭包知识点解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<body>
<button>测试1</button>
<button>测试2</button>
<button>测试3</button>

<script type="text/javascript">
//利用闭包
for (var i = 0,length=btns.length; i < length; i++) {
//此处的j是局部的,它将传入的[i]存入局部的[j]中,这样就能实现效果
(function (j) {
var btn = btns[j]
btn.onclick = function () {
alert('第'+(j+1)+'个')
}
})(i)
}
</script>
</body>

例子闭包分析

1
2
3
4
5
6
7
8
9
10
11
12
13
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)

初步分析:

  1. 行 1 - 8。在全局执行上下文中创建了一个新的变量createCounter,并赋值了一个的函数定义。

  2. 第 9 行。

    • 在全局执行上下文中声明了一个名为increment的新变量。

    • 需要调用createCounter函数并将其返回值赋给increment变量。

  3. 返回执行 行 1 - 8。调用函数,创建新的本地执行上下文。

    • 第 2 行。在本地执行上下文中,声明一个名为counter的新变量并赋值为 0;

    • 行 3 - 6。声明一个名为myFunction的新变量,变量在本地执行上下文中声明,变量的内容是为第 4 行和第 5 行所定义。

    • 第 7 行。返回myFunction变量的内容,删除本地执行上下文。变量myFunctioncounter不再存在。此时控制权回到了调用上下文。

  4. 再次回到 第 9 行

    • 在调用上下文(全局执行上下文)中,createCounter返回的值赋给了increment,变量increment现在包含一个函数定义内容为createCounter返回的函数。

    • 它不再标记为myFunction,但它的定义是相同的。在全局上下文中,它是的标记为 labeledincrement

  5. 第 10 行。声明一个新变量(c1)。

    • 继续第 10 行。查找increment变量,它是一个函数并调用它。它包含前面返回的函数定义,如第 4-5 行所定义的。

    • 创建一个新的执行上下文。没有参数。开始执行函数。

  6. 回到 第 4 行。

    • counter=counter + 1。在本地执行上下文中查找counter变量。

    • 只是创建了那个上下文,从来没有声明任何局部变量。看看全局执行上下文。这里也没有counter变量。

    • Javascript 会将其计算为 counter = undefined + 1,声明一个标记为counter的新局部变量,并将其赋值为 number 1,因为 undefined 被当作值为 0。 –>此处是错误的哦,别钻牛角尖,正确的理解在下方,此处是做错误对比

    • 第 5 行。变量counter的值(1),销毁本地执行上下文和counter变量。

  7. 回到第 10 行。返回值(1)被赋给 c1。

  8. 第 11 行。重复步骤 10-14,c2 也被赋值为 1。

  9. 第 12 行。重复步骤 10-14,c3 也被赋值为 1。

  10. 第 13 行。打印变量 c1 c2 和 c3 的内容。

它并不像从我上面的解释中所期望的那样记录 1,1,1。而是记录 1,2,3,函数记住的counter的变化

理解:

**无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包。闭包包含在函数创建时作用域中的所有变量,它类似于背包。函数定义附带一个小背包,它的包中存储了函数定义创建时作用域中的所有变 **

第二次分析(9~12行):

  1. 行 1 - 8。在全局执行上下文中创建了一个新的变量createCounter,并赋值了一个的函数定义。

  2. 第 9 行。

    • 在全局执行上下文中声明了一个名为increment的新变量。

    • 需要调用createCounter函数并将其返回值赋给increment变量。

  3. 返回执行 行 1 - 8。调用函数,创建新的本地执行上下文。

    • 第 2 行。在本地执行上下文中,声明一个名为counter的新变量并赋值为 0;

    • 行 3 - 6。声明一个名为myFunction的新变量,

    • 第5行.现在还创建了一个闭包,并将其作为函数定义的一部分。闭包包含作用域中的变量,在本例中是变量counter(值为0)。

    • 第7行,返回函数定义和它的闭包,闭包中包含了创建它时在作用域内的变量。

  4. 再次回到 第 9 行

    • 在调用上下文(全局执行上下文)中,createCounter返回的值赋给了increment,变量increment现在包含一个函数定义内容为createCounter返回的函数。

    • 它不再标记为myFunction,但它的定义是相同的。在全局上下文中,它是的标记为 labeledincrement

  5. 第 10 行。声明一个新变量(c1)。

    • 继续第 10 行。查找变量increment,它是一个函数,调用它。它包含前面返回的函数定义,如第4-5行所定义的。(它还有一个带有变量的闭包)。

    • 创建一个新的执行上下文。没有参数。开始执行函数。

  6. 回到 第 4 行。

    • 4行。[counter = counter + 1],寻找变量 [counter],在查找本地或全局执行上下文之前,检查一下闭包,闭包包含一个名为[counter]的变量,其值为0。在第4行表达式之后,它的值被设置为1。它再次被储存在闭包里,闭包现在包含值为1的变量 [counter]。
    • 5行。返回counter的值,销毁本地执行上下文。
  7. 回到第 10 行。返回值(1)被赋给 c1。

  8. 第 11 行。重复步骤10-14。这一次,在闭包中此时变量counter的值是1。它在第12行设置的,它的值被递增并以2的形式存储在递增函数的闭包中,c2被赋值为2

  9. 第 12 行。重复步骤 10-14,c3 被赋值为 3。

  10. 第 13 行。打印变量 c1 c2 和 c3 的内容。

是否有任何函数具有闭包,甚至是在全局范围内创建的函数?

答案是肯定的。在全局作用域中创建的函数创建闭包,但是由于这些函数是在全局作用域中创建的,所以它们可以访问全局作用域中的所有变量,闭包的概念并不重要。

但当函数返回函数时,闭包的概念就变得更加重要了。返回的函数可以访问不属于全局作用域的变量,但它们仅存在于其闭包中。

⭐闭包定义

在JavaScript中,闭包是一种特殊的对象,它允许一个函数访问并操作其外部作用域内的变量,即使该外部函数已经完成执行。简而言之,闭包是由函数及其相关的引用环境组合而成的实体。

闭包的形成需要满足以下条件:

  1. 内部函数:一个函数定义在另一个函数内部。
  2. 外部变量:内部函数需要访问外部函数的作用域中的变量。
  3. 持久引用:内部函数必须以某种方式被外部作用域中的变量所引用,以便在外部函数执行完毕后仍然可以访问内部函数。

组成:

  1. 函数:这是闭包的主体,是一个可以被调用的对象。
  2. 引用环境:这是一个保存了函数外部变量的环境记录,这些变量可以在函数内部被访问。

工作原理:

当一个函数被定义在一个外部函数内部时,这个内部函数可以访问外部函数的所有局部变量、参数以及内部定义的其他函数。当内部函数被返回或者以某种方式保存时,它就会形成一个闭包,因为它仍然能够访问其外部函数的作用域,即使外部函数已经执行完毕。

闭包作用

  1. 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
  2. 让函数外部可以操作(读写)到函数内部的数据(变量/函数)

问题:

  1. 函数执行完后, 函数内部声明的局部变量是否还存在?
    • 一般是不存在, 存在于闭中的变量才可能存在
  2. 在函数外部能直接访问函数内部的局部变量吗?
    • 不能, 但是可以通过闭包让外部操作它

闭包的生命周期

  1. 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)
  2. 死亡: 在嵌套的内部函数成为垃圾对象时
  • 即没有人指向它时死亡,通常置为[null],当然指向其他也行,但不安全(容易污染变量)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//闭包的生命周期
function fn1() {
//此时闭包就已经产生了(函数提升,实际上[fn2]提升到了第一行, 内部函数对象已经创建了)
var a = 2
function fn2 () { //如果时[let fn2=function(){}],那么在这行才会产生闭包
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4
f = null //闭包死亡(包含闭包的函数对象成为垃圾对象)

闭包的应用

闭包的应用 : 定义JS模块

  • 具有特定功能的js文件
  • 将所有的数据和功能都封装在一个函数内部(私有的)
  • 只向外暴露一个包信n个方法的对象或函数
  • 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
  1. 模块定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
//myModule.js
function myModule() {
//私有数据
var msg = 'My atguigu'
//操作数据的函数
function doSomething() {
console.log('doSomething() '+msg.toUpperCase())
}
function doOtherthing () {
console.log('doOtherthing() '+msg.toLowerCase())
}

//向外暴露对象(给外部使用的方法)
return {
doSomething: doSomething,
doOtherthing: doOtherthing
}
}

-----------------------------------------------------------------
// myModule2.js
(function () {
//私有数据
var msg = 'My atguigu'
//操作数据的函数
function doSomething() {
console.log('doSomething() '+msg.toUpperCase())
}
function doOtherthing () {
console.log('doOtherthing() '+msg.toLowerCase())
}

//向外暴露对象(给外部使用的方法)
window.myModule2 = {
doSomething: doSomething,
doOtherthing: doOtherthing
}
})()

  1. 模块调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//调用示例
------------ 模块调用1 --------------------------------------------
<script type="text/javascript" src="myModule.js"></script>
<script type="text/javascript">
var module = myModule()
module.doSomething()
module.doOtherthing()
</script>
------------ 模块调用2 --------------------------------------------
<script type="text/javascript" src="myModule2.js"></script>
<script type="text/javascript">
myModule2.doSomething()
myModule2.doOtherthing()
</script>

常见的闭包

将函数作为另一个函数的返回值

1
2
3
4
5
6
7
8
9
10
11
12
// 1. 将函数作为另一个函数的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1()
f() // 3
f() // 4

将函数作为实参传递给另一个函数调用

1
2
3
4
5
6
7
// 2. 将函数作为实参传递给另一个函数调用
function showDelay(msg, time) {
setTimeout(function () {
alert(msg)
}, time)
}
showDelay('atguigu', 2000

高阶函数与柯里化

  1. 从 ES6 高阶箭头函数理解函数柯里化(运用到闭包)
  1. 例子:
1
let add = a => b => a + b
  1. 以上是一个很简单的相加函数,把它转化成 ES5 的写法如下
1
2
3
4
5
6
7
function add(a) {
return function(b) {
return a + b
}
}
var add3 = add(3) //add3表示一个指向函数的变量 可以当成函数调用名来用
add3(4) === 3 + 4 //true
  1. 再简化一下,可以写成如下形式:
1
2
3
4
5
6
7
let add = function(a) {
var param = a;
var innerFun = function(b) {
return param + b;
}
return innerFun;
}
  1. 该函数的返回值是一个函数。这就是高阶函数的定义:以函数为参数或者返回值是函数的函数。

柯里化

  1. 图例:

image-20210415161137977

  1. 关键就是理解柯里化,其实可以把它理解成,柯里化后,将第一个参数变量存在函数里面了(闭包),然后本来需要n个参数的函数可以变成只需要剩下的(n - 1个)参数就可以调用,比如
1
2
3
4
5
6
7
8
9
10
11
12
let add = x => y => x + y
let add2 = add(2)
-*----------------------------------
本来完成 add 这个操作,应该是这样调用
let add = (x, y) => x + y
add(2,3)
----------------------------------
1. 而现在 add2 函数完成同样操作只需要一个参数,这在函数式编程中广泛应用。
let add = x => y => x + y
let add2 = add(2)
2.详细解释一下,就是 add2 函数 等价于 有了 x 这个闭包变量的 y => x + y 函数,并且此时 x = 2,所以此时调用
add2(3) === 2 + 3

闭包的缺点及解决

  1. 缺点:
  • 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
  • 容易造成内存泄露
  1. 解决:
  • 能不用闭包就不用

  • 及时释放

  • function fn1() {
      var arr = new Array(100000)
      function fn2() {
        console.log(arr.length)
      }
      return fn2
    }
    var f = fn1()
    f()
    f = null //让内部函数成为垃圾对象-->回收闭包