JS高级函数
JS高级函数
原型和原型链
原型[prototype]
函数的prototype属性
- 每个函数都有一个
prototype属性,它默认指向一个Object空对象(即为:原型对象) - 原型对象中有一个属性
constructor指向函数对象

给原型对象添加属性
- 作用:函数的所有实例对象都自动拥有原型中的属性(方法)
1 | // 每个函数都有一个prototype属性, 它默认指向一个Object空对象(即称为: 原型对象) |
显式原型与隐式原型
- 每个函数function都有一个
prototype,即显式原型(属性) - 每个实例对象都有一个[
__ proto __],可称为隐式原型(属性) - 对象的隐式原型的值为其对应构造函数的显式原型的值
- 内存结构:
1 | //定义构造函数 |
总结:
- 函数的[
prototype]属性: 在定义函数时自动添加的, 默认值是一个空Object对象 - 对象的[
__ proto __]属性: 创建对象时自动添加的,默认值为构造函数的prototype属性值 - 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)

原型链
原型链概念
访问一个对象的属性时,
- 先在自身属性中查找,找到返回
- 如果没有, 再沿着[
__ proto __]这条链向上查找, 找到返回 - 如果最终没找到, 返回undefined
1 | function Fn(){ |

- 别名: 隐式原型链
- 作用: 查找对象的属性(方法)
Object的原型对象的
_porto_指向的是null
构造函数/原型/实例对象之间的关系
1 | var o1 = new Object(); |

1 | function Foo(){ } |

1 | // 1.函数的显式原型指向的对象默认是空Object实例对象(Object不满足) |
属性问题
- 读取对象的属性值时: 会自动到原型链中查找
- 设置对象的属性值时: 不会查找原型链, 如果当前对象中没有此属性, 直接添加此属性并设置其值
- 方法一般定义在原型中, 属性一般通过构造函数定义在对象本身上(一般不同实例属性不同方法相同)
1 | function Fn() { } |
instanceof
- instanceof是如何判断的?
- 表达式: A instanceof B
- 如果B函数的显式原型对象在A对象的原型链上, 返回true, 否则返回false
- Function是通过new自己产生的实例
1 | /* |
面试题
题目1:
1 | function A () {} |

题目二:
1 | function F (){} |

执行上下文和执行上下文栈
变量提升和函数提升
- 变量声明提升
- 通过var定义(声明)的变量, 在定义语句之前就可以访问到
- 值: undefined
- 函数声明提升
- 通过function声明的函数, 在之前就可以直接调用
- 值: 函数定义(对象)
变量和函数的提升是把变量或者函数提升到其作用域的最前面
- 引出一个问题: 变量提升和函数提升是如何产生的?
1 | var a = 3 |
执行上下文
- 代码分类(按照代码位置划分)
- 全局代码
- 函数(局部)代码
全局执行上下文
在执行全局代码前将window确定为全局执行上下文
对全局数据进行预处理
var定义的全局变量==>undefined, 添加为window的属性
function声明的全局函数==>赋值(fun), 添加为window的方法
this==>赋值(window)
开始执行全局代码
函数执行上下文
在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
对局部数据进行预处理
形参变量==>赋值(实参)==>添加为执行上下文的属性
arguments==>赋值(实参列表), 添加为执行上下文的属性var定义的局部变量==>undefined, 添加为执行上下文的属性
function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
this==>赋值(调用函数的对象)
开始执行函数体代码
执行上下文栈
在全局代码执行前, JS引擎就会创建一个栈来存储管理所有的执行上下文对象
在全局执行上下文(window)确定后, 将其添加到栈中(压栈)–>
所以栈底百分百是[window]在函数执行上下文创建后, 将其添加到栈中(压栈)
在当前函数执行完后,将栈顶的对象移除(出栈)
当所有的代码执行完后, 栈中只剩下window
上下文栈数==函数调用数+1`
1 | //1. 进入全局执行上下文 |

面试题
函数提升优先级高于变量提升,且不会被变量声明覆盖,但是会被变量赋值覆盖
1 | /* |
作用域与作用域链
作用域
- 理解
- 一串代码所在的区域
- 作用域是静态的(相对于上下文对象),在代码编写时候就已经确定
- 分类
- 全局作用域
- 函数作用域
- 没有块作用域(ES6 中:作用域块)
- 作用
- 隔离变量,不用作用域下同名变量不会有冲突
1 | /* //没块作用域 |
作用域与执行上下文的区别与联系
- 区别1: 创建的时间
- 全局作用域之外,每个函数都会创建自己的作用域作用域在函数定义时就已经确定了,而不是在函数调用时
- 全局执行上下文环境是在全局作用域确定之后, js代码马上执行之前创建
- 函数执行上下文是在调用函数时, 函数体代码执行之前创建
- 区别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,然后定义了两个函数fn和show。fn函数简单地打印变量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,因为fn2在fn2函数的作用域中并没有被声明或定义为一个局部变量。在 JavaScript 中,函数体内部的作用域会首先查找局部变量,如果没有找到,则会继续向外层作用域查找,直到全局作用域。但是,在这个例子中,fn2是作为obj对象的一个属性(方法)存在的,而不是作为任何函数内部的局部变量或全局变量。当在
fn2函数内部尝试访问fn2时,JavaScript 会认为您是在尝试访问一个名为fn2的局部变量,但实际上并没有这样的局部变量存在。因此,它会继续向外层作用域查找,但同样找不到,因为fn2不是外层作用域(在这个例子中是全局作用域)的一部分。
闭包知识预备
例子分析执行上下文
1 | 1: let a = 3 |
上述代码在js引擎里具体工作流程:
- 第一行,全局上下文声明了一个新变量a,赋值为3
- 第2行到第5行:
- 在全局执行上下文中声明了一个
addTwo的新变量,给它分配了一个函数定义 - 两个
{}内的所有内容分配个addTwo,函数内部的代码没有被求值,没有执行,只是储存在一个变量中以备将来使用.
- 在全局执行上下文中声明了一个
- 在第6行:
- 在全局执行上下文中声明一个新变量,将其标记为b,变量一声明值为
undefined - 遇到赋值操作符,准备给b赋值,看到一个函数被调用(每个函数都会返回一些东西:值,对象,undefined)无论函数返回什么,都将其赋值给b
- 调用标记为
addTwo的函数,JS将在全局执行上下文内存中查找addTwo,返现它在第2到第5行中定义,包含一个函数定义 - 变量a作为参数传递给函数,JS在全局执行上下文中搜索变量a,找到发现值为3,将3作为参数传递给函数,准备执行函数
- 执行上下文切换,创建一个本地执行上下文
addTwo执行上下文,执行上下文被推送到调用堆栈上,开始函数执行:- 值3是作为一个参数传递的,第一步要在
addTwo执行上下文中创建一个变量x,将3赋值给x - 执行第三行,声明一个新变量ret,设置为
undefined - 执行相加操作,得到的值赋值给ret
- 执行第四行,返回ret内容,函数执行完毕
- 值3是作为一个参数传递的,第一步要在
addTwo 执行上下文被销毁,变量x和ret被消去了,它们已经不存在了。addTwo 执行上下文从调用堆栈中弹出,返回值返回给调用上下文,在这种情况下,调用上下文是全局执行上下文,因为函数addTwo是从全局执行上下文调用的。第6行执行完毕
- 在全局执行上下文中声明一个新变量,将其标记为b,变量一声明值为
- 第 7 行,
b的值 5 被打印到控制台了。
例子分析词法作用域
上文中:一个函数可以访问在它的调用上下文中定义的变量,这个就是词法作用域(Lexical scope)****
返回函数的函数[高阶函数]
1 | 1: let val = 7 |
闭包
一个函数和对其周围状态(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 | 1: function createCounter() { |
初步分析:
行 1 - 8。在全局执行上下文中创建了一个新的变量
createCounter,并赋值了一个的函数定义。第 9 行。
在全局执行上下文中声明了一个名为
increment的新变量。需要调用
createCounter函数并将其返回值赋给increment变量。
返回执行 行 1 - 8。调用函数,创建新的本地执行上下文。
第 2 行。在本地执行上下文中,声明一个名为
counter的新变量并赋值为 0;行 3 - 6。声明一个名为
myFunction的新变量,变量在本地执行上下文中声明,变量的内容是为第 4 行和第 5 行所定义。第 7 行。返回
myFunction变量的内容,删除本地执行上下文。变量myFunction和counter不再存在。此时控制权回到了调用上下文。
再次回到 第 9 行
在调用上下文(全局执行上下文)中,
createCounter返回的值赋给了increment,变量increment现在包含一个函数定义内容为createCounter返回的函数。它不再标记为
myFunction,但它的定义是相同的。在全局上下文中,它是的标记为labeledincrement。
第 10 行。声明一个新变量(c1)。
继续第 10 行。查找
increment变量,它是一个函数并调用它。它包含前面返回的函数定义,如第 4-5 行所定义的。创建一个新的执行上下文。没有参数。开始执行函数。
回到 第 4 行。
counter=counter + 1。在本地执行上下文中查找
counter变量。只是创建了那个上下文,从来没有声明任何局部变量。看看全局执行上下文。这里也没有
counter变量。Javascript 会将其计算为 counter = undefined + 1,声明一个标记为
counter的新局部变量,并将其赋值为 number 1,因为 undefined 被当作值为 0。–>此处是错误的哦,别钻牛角尖,正确的理解在下方,此处是做错误对比第 5 行。变量
counter的值(1),销毁本地执行上下文和counter变量。
回到第 10 行。返回值(1)被赋给 c1。
第 11 行。重复步骤 10-14,c2 也被赋值为 1。
第 12 行。重复步骤 10-14,c3 也被赋值为 1。
第 13 行。打印变量 c1 c2 和 c3 的内容。
它并不像从我上面的解释中所期望的那样记录 1,1,1。而是记录 1,2,3,函数记住的counter的变化
理解:
**无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包。闭包包含在函数创建时作用域中的所有变量,它类似于背包。函数定义附带一个小背包,它的包中存储了函数定义创建时作用域中的所有变 **
第二次分析(9~12行):
行 1 - 8。在全局执行上下文中创建了一个新的变量
createCounter,并赋值了一个的函数定义。第 9 行。
在全局执行上下文中声明了一个名为
increment的新变量。需要调用
createCounter函数并将其返回值赋给increment变量。
返回执行 行 1 - 8。调用函数,创建新的本地执行上下文。
第 2 行。在本地执行上下文中,声明一个名为
counter的新变量并赋值为 0;行 3 - 6。声明一个名为
myFunction的新变量,第5行.现在还创建了一个闭包,并将其作为函数定义的一部分。闭包包含作用域中的变量,在本例中是变量
counter(值为0)。第7行,返回函数定义和它的闭包,闭包中包含了创建它时在作用域内的变量。
再次回到 第 9 行
在调用上下文(全局执行上下文)中,
createCounter返回的值赋给了increment,变量increment现在包含一个函数定义内容为createCounter返回的函数。它不再标记为
myFunction,但它的定义是相同的。在全局上下文中,它是的标记为labeledincrement。
第 10 行。声明一个新变量(c1)。
继续第 10 行。查找变量
increment,它是一个函数,调用它。它包含前面返回的函数定义,如第4-5行所定义的。(它还有一个带有变量的闭包)。创建一个新的执行上下文。没有参数。开始执行函数。
回到 第 4 行。
- 第
4行。[counter = counter + 1],寻找变量 [counter],在查找本地或全局执行上下文之前,检查一下闭包,闭包包含一个名为[counter]的变量,其值为0。在第4行表达式之后,它的值被设置为1。它再次被储存在闭包里,闭包现在包含值为1的变量 [counter]。 - 第
5行。返回counter的值,销毁本地执行上下文。
- 第
回到第 10 行。返回值(1)被赋给 c1。
第 11 行。重复步骤
10-14。这一次,在闭包中此时变量counter的值是1。它在第12行设置的,它的值被递增并以2的形式存储在递增函数的闭包中,c2被赋值为2。第 12 行。重复步骤 10-14,c3 被赋值为 3。
第 13 行。打印变量 c1 c2 和 c3 的内容。
是否有任何函数具有闭包,甚至是在全局范围内创建的函数?
答案是肯定的。在全局作用域中创建的函数创建闭包,但是由于这些函数是在全局作用域中创建的,所以它们可以访问全局作用域中的所有变量,闭包的概念并不重要。
但当函数返回函数时,闭包的概念就变得更加重要了。返回的函数可以访问不属于全局作用域的变量,但它们仅存在于其闭包中。
⭐闭包定义
在JavaScript中,闭包是一种特殊的对象,它允许一个函数访问并操作其外部作用域内的变量,即使该外部函数已经完成执行。简而言之,闭包是由函数及其相关的引用环境组合而成的实体。
闭包的形成需要满足以下条件:
- 内部函数:一个函数定义在另一个函数内部。
- 外部变量:内部函数需要访问外部函数的作用域中的变量。
- 持久引用:内部函数必须以某种方式被外部作用域中的变量所引用,以便在外部函数执行完毕后仍然可以访问内部函数。
组成:
- 函数:这是闭包的主体,是一个可以被调用的对象。
- 引用环境:这是一个保存了函数外部变量的环境记录,这些变量可以在函数内部被访问。
工作原理:
当一个函数被定义在一个外部函数内部时,这个内部函数可以访问外部函数的所有局部变量、参数以及内部定义的其他函数。当内部函数被返回或者以某种方式保存时,它就会形成一个闭包,因为它仍然能够访问其外部函数的作用域,即使外部函数已经执行完毕。
闭包作用
- 使用函数内部的变量在函数执行完后, 仍然存活在内存中(延长了局部变量的生命周期)
- 让函数外部可以操作(读写)到函数内部的数据(变量/函数)
问题:
- 函数执行完后, 函数内部声明的局部变量是否还存在?
- 一般是不存在, 存在于闭中的变量才可能存在
- 在函数外部能直接访问函数内部的局部变量吗?
- 不能, 但是可以通过闭包让外部操作它
闭包的生命周期
- 产生: 在嵌套内部函数定义执行完时就产生了(不是在调用)
- 死亡: 在嵌套的内部函数成为垃圾对象时
- 即没有人指向它时死亡,通常置为[
null],当然指向其他也行,但不安全(容易污染变量)
1 | //闭包的生命周期 |
闭包的应用
闭包的应用 : 定义JS模块
- 具有特定功能的js文件
- 将所有的数据和功能都封装在一个函数内部(私有的)
- 只向外暴露一个包信n个方法的对象或函数
- 模块的使用者, 只需要通过模块暴露的对象调用方法来实现对应的功能
- 模块定义:
1 | //myModule.js |
- 模块调用
1 | //调用示例 |
常见的闭包
将函数作为另一个函数的返回值
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
高阶函数与柯里化
- 从 ES6 高阶箭头函数理解函数柯里化(
运用到闭包)
- 例子:
1 let add = a => b => a + b
- 以上是一个很简单的相加函数,把它转化成 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
2
3
4
5
6
7 let add = function(a) {
var param = a;
var innerFun = function(b) {
return param + b;
}
return innerFun;
}
- 该函数的返回值是一个函数。这就是
高阶函数的定义:以函数为参数或者返回值是函数的函数。
柯里化
- 图例:
- 关键就是
理解柯里化,其实可以把它理解成,柯里化后,将第一个参数变量存在函数里面了(闭包),然后本来需要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
闭包的缺点及解决
- 缺点:
- 函数执行完后, 函数内的局部变量没有释放, 占用内存时间会变长
- 容易造成内存泄露
- 解决:
能不用闭包就不用
及时释放
function fn1() { var arr = new Array(100000) function fn2() { console.log(arr.length) } return fn2 } var f = fn1() f() f = null //让内部函数成为垃圾对象-->回收闭包


