引子: 为什么要有函数?在某个地方重复的逻辑,为了可以复用,减少代码量而拒绝 CV,可以封装成一个函数进行调用。
函数也是引用数据类型,是通过引用地址进行相关操作的。
可能会导致作用域链未释放。
在 JavaScript 中,函数是一种特殊的对象,都有prototype属性,其值为“原型对象”。该原型对象有两个属性__proto__和constructor,__proto__属性指向当前对象的原型对象,constructor属性指向当前对象的构造函数。
1. 创建方式(3 种)
1-1. 声明
始终是默认的全局对象。
1-2. 表达式
存储在变量中。 必须先声明再使用。
1-3. 构造
let fn = new Function(console.log(8))
2. 函数调用方式(4 种)
2-1. 函数名()
2-2. 函数作为方法调用
2-3. new 构造函数调用
2-4. call()、apply()
面试经典例题:apply,call,bind三者的区别
三者都可以传参,但是apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入。 bind 是返回绑定this之后的函数,便于稍后调用;apply 、call 则是立即执行。
重点: bind不会自动调用函数,必须后续手动调用函数执行;而apply、call会绑定this并且自动调用函数执行。
3. 函数执行
函数可以分为普通函数和构造函数,创建机制相同,所以其执行机制可以分别去观察。
函数创建 是用来存储函数体代码的。
3-1. 整体描述
函数创建时,在堆内存中存储字符串。函数执行时,会创建一块栈内存空间,初始化其中的变量,执行函数体,当函数执行完毕时,这块内存空间会被销毁,其中的变量也随之销毁。下次调用该函数时会重新开辟一块新内存空间,重复上述操作。
但这种机制会产生问题。wrap()执行完毕,其所占用的栈内存空间被销毁,里面的变量 a 也随之销毁。但 inner()消费了 a 变量,同时 inner 函数被抛出到 wrap 外部,如果此时执行 result(),因 a 变量已经被销毁,所以无法输出 a。此时,这种函数执行机制就产生了问题。
下面代码会在普通函数创建、执行作图详解。
为了解决该问题,诞生了函数执行的保护机制,在上述例子中,预解析器会将栈中的变量复制到堆中,下次执行 result 时直接使用堆中的引用,这种保护机制我们称之为闭包。V8 使用惰性解析加速 JS 启动速度,同时减少内存空间的占用,但同时为了能够分析出函数中是否存在闭包,需 V8 引入预解析器。
预解析器的作用有 2 个: 1.判断当前函数是否存在语法错误 2.函数内部是否引用外部变量 如果存在引用外部变量,则将当前执行上下文中的变量从栈复制到堆中。(这也导致作用域链没有释放)
下面我们再详细看看 普通函数 和 构造函数 的创建、执行细节。
3-2. 普通函数
3-2-1. 创建
(1)开辟新的堆内存空间,记录堆内存地址, 把函数中的代码作为字符串存储在其中。 (2)在当前上下文中 声明该函数,函数声明和定义提升。 (3)把堆内存地址赋值给 函数名。
这样子画图是非常贴切的,其实自己可以尝试一下,会更加印象深刻。
3-2-2. 执行
(1)在栈内存中,形成函数作用域,提供代码执行的环境。栈也可以说是执行上下文栈,即 stack。 (2)把堆内存中的字符串复制到新开辟的栈内存空间,变成真正的 JS 代码。 (3)对得到的 JS 代码,进行形参赋值、变量提升和函数提升。 (4)执行 JS 代码 (5)若引用外部变量,则 CV 至堆内存中。执行完毕销毁栈内存空间。