1. 执行上下文
1.1 重要概念
- 全局对象
- js 引擎在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
- 该对象所有的作用域(scope) 都可以访问;
- 里面会包含Date、Array、String、Number、setTimeout、setInterval等等;
- 其中,还有一个window属性指向自己
- js 引擎在执行代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
- 执行上下文
- js 引擎内部有一个执行上下文(Execution Context Stack,简称 ECS), 它是用于执行代码的调用栈。
-
一开始,执行的是全局的代码块:
- 全局的代码块为了执行会构建一个 Global Execution Context(GEC);
- GEC 会被放入到 ECS 中执行;
-
GEC 被放入到 ECS 中里面包含两部分内容:
- 第一部分: 在执行代码前,在
parser 转成 AST 的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值;- 这个过程也称之为
变量的作用域提升(hoisting)
- 这个过程也称之为
- 第二部分: 在代码执行中,对变量赋值,或者执行其他的函数;
- 第一部分: 在执行代码前,在
-
VO对象
- 每一个执行上下文会关联一个
VO(Variable Object,变量对象),变量和函数声明会被添加到这个 VO 对象中。 - 当全局代码被执行的时候,VO 就是 GO 对象了
- 每一个执行上下文会关联一个
-
函数执行上下文
- 在执行的过程中执行到一个函数时,就会根据函数体创建一个函数执行上下文(Functional Execution Context,简称 FEC) ,并且压入到 EC Stack 中。
- 因为每个执行上下文都会关联一个 VO,那么函数执行上下文关联的 VO 是什么呢?
- 当进入一个函数执行上下文时,会创建一个 AO 对象(Activation Object);
- 这个 AO 对象会使用 arguments 作为初始化,并且初始值是传入的参数;
- 这个 AO 对象会作为执行上下文的 VO 来存放变量的初始化;
1.2 上述代码执行流程
-
在执行代码之前,会在堆内存中创建一个全局对象(GO)。此外,代码是在全局中执行的,并且执行代码是放在执行栈(ECS) 中被执行的,所以 js 引擎会创建一个全局执行上下文(GEC) ,并将这个执行上下文放到 ECS 中;
-
首先,会对代码进行解析,这一阶段只是将全局定义的变量、函数等加入到 GO 中,并不会赋值;由于每个执行上下文都会关联一个 VO 对象,此阶段 VO 指向 GO;
- 函数声明会放到变量、对象声明之前,并且在解析时,普通对象并不会分配新的内存,和变量声明方式相同
- 因此,首先,给 函数 bar、foo 分配新的内存空间;然后,声明变量 message,值为 undefined;对象 obj 值也为 undefined
- PS: 定义一个函数 foo,然后在函数前面执行代码
var foo = 'abc',此时 foo 的值为 abc
var foo = 'abc' console.log(foo) // abc function foo() { ... } -
执行代码
- 将 message 值变为
Global Message; - 给对象 obj 创建一块内存空间,此时 obj 就保存的是内存空间的地址;
- 执行函数 foo,会给函数体创建一个函数执行上下文(FEC),并且压入到 ECS 中,并为此函数体创建一个 AO 对象;
- 这个 AO 对象会使用 arguments 作为初始化,并且初始值是传入的参数,因此,解析过程时,会声明函数 bar,为 bar 创建一块内存空间,bar 指向此内存空间,
- message 值为 undefined,
- height 值为 undefined,
- num 值为 undefined
- 执行 AO 中代码,message 赋值为 "Foo Message",然后,执行 bar(), 创建方式同 foo;
- 为 bar 创建一个 FEC,并压入 ECS 中,然后为此函数创建一个 AO 对象,解析函数,声明 address 值为 undefined
- 执行函数代码,打印代码,address 赋值为 bar
- 执行完成后,bar 函数对应的 VO 会出栈,相应堆中的 AO 对象也会被销毁(会不会销毁取决于 GC 垃圾回收器),之后继续执行 foo 中后面的代码
- age 赋值为 18,height 赋值为 1.88,执行打印代码
- 代码执行完成后,foo 对应的 VO 出栈,然后执行全局对象(GO)中的代码。
- 将 message 值变为
-
代码执行完成
1.3 作用域链
-
原理解析
- 首先会在栈内存中创建一个GO(window)对象,此时全局上下文中的VO对象指向的是GO。而GO对象中首先会初始化,message值为undefined;由于foo是一个函数,会创建一个foo的函数对象,此函数对象会使用arguments作为初始化,包含length、name等属性。还包括一个scopes作用域链,此作用域链是一个列表,第一个指向GO。
- 创建完GO对象后,会执行全局代码,首先执行的 foo 函数,会为该函数创建一个foo AO 对象,bar是一个函数,在内存中会创建一个bar函数对象,此函数对象也包括length、name等属性,还包含scopes作用域链,此作用域链的列表含两个值:0(foo AO对象),1:GO对象;name初始化为 undefined。然后执行 foo 中的代码,将 name 的值赋值为 foo,并将 bar 函数返回。
- foo执行上下文结束后,会从栈中被弹出。
- 之后为 bar 创建一个 执行上下文VO,指向的是 bar AO 对象,使用 arguments 作为初始化。然后执行函数中的代码,由于此 AO 对象中不包括 name 属性,所以会沿着作用域链优先去 foo AO 对象中去找,找到后打印 name。
-
函数作用域链
- 函数的作用域链在一开始创建(解析)的时候就已经确定,跟它的调用位置没有关系
var message = 'Global Message' function foo() { console.log(message) // Global Message } foo() var obj = { name: 'obj', bar: function() { var message = 'bar message' foo() } } obj.bar()- 在上述代码中,执行函数打印的结果为
Global Message,因为函数创建的时候是在全局作用域创建的,所以它的作用域链列表第一个指向的是 GO。
-
面试题
function foo() {
var a = b = 100
}
foo()
console.log(a) // Error: a is not defined
// 由于 b 前面没有声明,所以浏览器会将其作为全局对象,但是这种写法不严谨,不推荐这样写
console.log(b) // 100
PS:这样设计的目的是为了闭包。 方便我们更好的编写代码。
1.4 内存管理
-
代码执行过程需要为它分配内存,有些编程语言手动管理内存,有些自动帮助我们管理内存
-
内存管理生命周期:
- 分配内存;
- 使用分配的内存;
- 原始数据类型内存的分配:直接在栈空间进行分配;
- 复杂数据类型内存的分配:在堆内存中进行分配;
- 不需要使用时,释放内存;
-
垃圾回收机制
-
Garbage Collection 简称 GC。最早出现在 Lisp 语言中。
-
- 引用计数(Reference counting)
- retainCount 有一个对象有引用它时,那么这个对象的引用就+1。当计数值为 0 的时候就会被销毁。
- 弊端:会产生循环引用
-
- 标记清除
- 核心思路:可达性
- 设置一个根对象(window对象),垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,对于没有引用到的对象,就认为是不可用的对象,那么就将它回收。
- 可以很好地解决循环引用的问题。
-
- 标记整理
- 回收期间同时会将保留的存储对象搬运汇集到连续的内存管理,从而整合空闲空间,避免内存碎片化。
-
- 分代收集:对象会被分成两组:新的和旧的
- 许多对象出现,完成它们的工作并很快死去,它们可以很快被清理;
- 哪些长期存活的对象会变得老旧,而且被检查的频次也会减少。
1.5 闭包
- 一个普通的函数 function ,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包;
- 从广义的角度来说:JavaScript中的函数都是闭包;
- 从狭义的角度来说:JavaScript中一个函数,如果访问了外层作用域的变量,那么它就是一个闭包;
为什么会存在闭包,因为闭包的存在能够让我们很好的调用外部变量。 请看下面实例:如果没有闭包,那么我们在使用外部变量的时候,首先在函数定义的时候要一一讲这些形参,其次,在调用的时候还有一一讲这些参数传进入,使用起来相当麻烦,因此闭包的出现大大解决了这个问题。
var name = 'global name'
var age = 18
var height = 1.88
var addres = 'bj'
var intro = 'large'
function foo(name, age, height, address, intro, num1, num2) {
var message = 'Hello World'
console.log(message, name, height, address, intro)
function bar() {
console.log(name)
}
bar()
}
foo(name, age, height, address, intro, 20, 30)
-
浏览器优化
- 在执行内部函数的时候,如果没有引用外部变量,浏览器会自动将没有引用到的参数销毁
1.6 内存泄漏
- 对于那些永远不会再使用的对象,但是对于GC来说,也不知道要进行释放,对应内存会依然保留着。
- 解决方法:只需要将其指针指向 null
2. 函数属性、arguments、函数柯里化以及组合函数
2.1 函数属性
- 最重要的是name、length属性。
2.2 arguments
-
类数组,含有 length 属性,但是不包括 push、slice 等方法。
-
类数组转数组的几种方法:
-
遍历 arguments,依次添加到数组;
-
数组的 slice 方法:
- Array.prototype.slice.call(arguments)
- [].slice.call(arguments)
-
ES6 方法:
- [...arguments]
- Array.from()
-
-
箭头函数不绑定 arguments,会去上层作用域查找。
-
函数的 rest 剩余参数:可以将不定数量参数放到一个数组中。
-
rest 与 arguments 的区别:
- 剩余参数只包含那些没有对应形参的实参,而arguments对象包含了传给实参的所有实参;
- arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
- arguments是早期ECMAScript中为方便获取所有的参数提供的一个数据结构,而 rest参数是 ES6中提供并且来代替arguments的
-
剩余参数必须放到最后一个位置,否则会报错。
2.3 纯函数
- 确定的输入,一定会产生确定的输出;
- 函数在执行过程中,不能产生副作用;
2.4 副作用
- 在执行函数时,除了返回函数值,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;
2.5 柯里化
2.5.1 概念
- 只传递给函数一部分参数来调用它,让它返回一个函数去处理剩余的参数,这个过程就称之为柯里化。
2.5.2 优势
- 函数的职责单一
- 在函数编程中,我们其实希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理;
- 我们可以将每次传入的参数在单一的函数中进行处理,处理完之后在下一个函数中在使用处理后的结果
- 函数的参数复用
2.5.3 实现自动函数柯里化
function foo1(x, y, z) {
console.log(x + y + z)
}
// foo1 柯里化实现效果
const foo2 = x => y => z => console.log(x + y + z)
// 自动柯里化函数
function customCurrying(fn) {
// 分为两类:
// 1. 继续返回一个新的函数,继续接受其他参数
// 2. 直接执行fn的函数
function curried(...args) {
if (args.length >= fn.length) { // 执行第二类
// return fn(...args) // 不包含 this
return fn.apply(this, args)
} else { // 执行第一类
return function (...args2) {
// return curried(...args.concat(args2))
return curried.apply(this, args.concat(args2))
}
}
}
return curried
}
var fooCurry = customCurrying(foo1)
fooCurry(10)(20)(30)
2.6 组合函数
2.6.1 概念
- 对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的;
- 每次都需要进行两个函数的调用,操作上就会显得重复;
- 将这两个函数组合起来,实现自动依次调用,这个过程就称为组合函数。
2.6.2 实现
// 封装的函数:传入多个函数,自动将多个函数组合在一起依次调用
function composeFn(...fns) {
// 1. edge case
var length = fns.length
if (length <= 0) return
for (var i = 0; i < length; i++) {
var fn = fns[i]
if (typeof fn !== 'function') {
throw new Error('index position ${i} must be function')
}
}
// 2. 返回的新函数
return function (...args) {
var result = fns[0].apply(this, args)
for (var i = 1; i < length; i++) {
var fn = fns[i]
result = fn.apply(this, [result])
}
return result
}
}
// 第一步对数字*2
function double(num) {
return num * 2
}
// 第二步对数字**2
function pow(num) {
return num ** 2
}
var newFn = composeFn(double, pow, console.log)
newFn(100)
2.7 call & apply
call、apply 使用场景:
- 一般需要在一个对象中调用另一个对象的方法。这个方法可能在两个对象中都存在,因此需要明确指定要使用另一个对象的方法。
- 绑定 this
区别:
- call:Function.call(obj, params1, params2, ...)
- apply: Function.apply(obj, [params1, params2, ...])