本章概要
本来只想讲闭包,但是要把闭包真的弄清楚.牵扯的东西比较多,所以想放在一起.通过本章,你将会弄明白 作用域 作用域链 词法作用域 执行上下文 this指向 闭包 等js基础知识
作用域和作用域链
作用域的概念
作用域查就是变量的规则,相当于将变量包裹,隔离起来.域外不能访问域内.
js中有哪些作用域
- 全局作用域
- 函数作用域
- 块级作用域(Es6新增)
作用域链
多个作用域嵌套后,就有层级结构.如果在当前作用域不能找到变量,就会沿着外层作用域一直往外找,直到找到最外层.这种类似链条的关系就是我理解的作用域链.
变量提升
在a,b声明前打印a,b结果如下
js
console.log(b,a) // f b(){} undefined
function b(){
console.log('ji')
}
var a = 2
- var声明的变量以及非表达式声明的函数,编译器会在当前作用域顶部,创建同名变量
- 函数赋值为与变量同名的空函数体,否则为undefined
- 函数与变量同名, 函数优先级高
- 如果重复声明,后者覆盖前者
执行阶段
js
function b(){
console.log(c) //Uncaught ReferenceError: c is not defined
c = 2
}
b()
// console.log(c) // 2
运行上面的代码,报错 Uncaught ReferenceError.注释掉函数内部打印,在外面打印又是2,为什么?这是时候要引出两个概念?LHS RHS 等号右边,取值可以称之为RHS, 赋值操作称之为LHS
变量查找的武器
- RHS 沿着作用域链往上查找,如果最外层没有找到,报错Uncaught ReferenceError
- LHS 沿着作用域链往上查找,如果最外层没有找到,会在最外层创建一个同名变量并赋值
了解以上知识,我们也许会思考一个问题.函数的作用域到底是执行的时候确定的呢还是声明的时候确定呢? 答案是声明的时候,就已经确定了.因为js的作用域是基于词法作用域.
词法作用域
比如说,我在一个函数内部抛出一个函数,在全局又接收了这个函数并运行.此时执行函数的作用域是全局还是依然在他声明的地方. 此时,词法作用域的作用就出来了.作用域的层级关系是静态的.在声明的时候,就已经确定了.取决于代码的位置,不是执行的位置. 这就是我理解的词法作用域
js代码是如何执行的
准备工作
当要执行一段js脚本的时候,js引擎会提供一个栈结构的东西,保证js的代码从上到下有序执行.这个栈就是 调用栈 还会为js脚本提供一个叫做 全局上下文 的东西.并把全局上下文压入栈底. 并在全局上下文上创建global对象,用来存储全局的变量. global对象指向window.简单总结如下:
- 创建全局上下文
- 创建global对象指向window
- 压入栈底
执行上下文
上面已经对 全局上下文 简单的介绍,保存js代码在执行时需要用的信息一种抽象概念.除了全局上下文,还有 函数上下文 , eval上下文.eval上下文可忽略,官方并不推荐使用.
函数上下文
执行函数前准备阶段.
- 绑定this
- 创建变量对象VO,存储函数内部的变量
- 确定作用域链
执行函数
- 将函数上下文入栈
- 如果内部还有函数执行,将内部函数入栈,递归
- 栈内从顶部依次执行出栈
- 直到调用栈为空,整个js脚本执行结束
探索闭包
前面讲了 作用域链 简单的编译过程 函数执行上下文 调用栈 这么多东西,其实就是为了闭包做铺垫.这也是笔者在探索闭包的过程中,看了大量的文章,也参阅了一些经典的js书籍,其实大部分讲的还是概览,并未揭露本质.先从最简单的🌰开始:
js
function out(){
var b = 't'
function inner(){
console.log(b)
}
return inner
}
var innerFn = out()
innerFn()
显而易见inner引用了out中b,调用innerFn()的时候,在innerFn的作用域链上,多了一个Closuer(out),产生了闭包
思考
1.不调用innerFn()会产生闭包吗?只执行out
js
function out(){
var b = 't'
function inner(){
console.log(b)
}
return inner
}
out()
2.不抛出inner会产生吗?
js
function out(){
var b = 't'
function inner(){
console.log(b)
}
}
out()
有以上两个疑问,因为在调试的时候,inner没有调用,在调用堆栈中一直找不到Clouser.后来才明白,闭包存在于内部函数的作用域链中.内部函数调用,我们在控制台能显示的看的到闭包.那如果内部函数没调用呢?回到起点逆向分析:
js
function out(){
var b = 't'
function inner(){
console.log(b)
}
return inner
}
var innerFn = out()
innerFn()
out()函数执行完,out执行上下文已经出栈了,out中b也随着上下文清楚了.但我们再次调用inneFn()的时候,依然能拿到b.说明此时内部的变量b不是真正的外部函数的b.造成这种现象的原因是什么呢?经过笔者不停的查阅资料,跟 V8预编译 V8惰性解析 有关.有兴趣的同学可以查看详细的资料,这里只做简单的介绍.
V8惰性解析 为了节约内存,遇到函数的时候,并不会真正的解析它.比如 funtion a(){...},会以 a = f a(){}的形式保存在作用域中.只有真正调用的时候,才会去赋值操作.但是,有一种特殊情况,闭包.那么V8又是怎么处理闭包的呢?
V8预解析 当执行一个函数的时候,如果函数内部还有函数的话,V8会对这个函数进行预解析.扫描这个函数内部是否引用了外层函数的变量.如果引用了,就把该变量捕获到内存中去.就形成了闭包
所以闭包是什么呢?当调用一个函数的时候,如果内部函数引用外部函数变量,编译器预解析,就会产生闭包.
什么是this
前面已经讲到,当调用函数的时候,会先创建函数上下文,并给上下文绑定一个this对象.因此,this就是函数上下文上的一个属性.那么this的值如何确定呢?
绑定this的规则
1.默认绑定
直接调用的话,默认绑定到全局对象window
js
function foo(){}
foo()
2.隐士绑定
- 由一个对象去调用,this绑定到该对象
- 隐士链式调用
- 隐士丢失
js
var a = 3
var obj2 = {
a: 4,
foo(){
console.log(this.a) // 4 隐士链式调用
}
}
var obj = {
a: 2,
foo(){
console.log(this.a) // 2
},
obj2: ojb2
}
obj.foo() // 2 隐士调用 调用函数的位置 this obj
obj.obj2.foo() // 调用函数 的位置 this obj2
var b = obj.foo()
b() // 3 隐士调用丢失 调用函数的位置 window
总结: 不管如何调用,要找到函数调用的位置,因为this是在调用的时候绑定的.找到调用的位置,找到最后一个调用者.
3.显示绑定
通过call,bind,apply函数改变this的指向,参考call内部实现原理
4.new绑定
下一节会详细介绍new的内部实现
优先级:new > 显示绑定 > 隐士绑定 > 默认绑定
new内部实现原理
js
function myNew(Fun){
// 创建一个空对象
let obj = {}
// 对象的原型指向构造函数的原型 ojb就能访问Fun原型上的属性和方法了
obj.__proto__ = Fun.prototype
//绑定this
const res = Fun.apply(obj,...args)
//处理返回值 不是对象就返回obj
return res instanceof Object ? res : obj
}
总结
本文设计到的知识点有: 作用域 作用域链 变量提升 LHS RHS 闭包 执行上下文 this指向 new内部原理
- 闭包是编译器预编译导致的
- this是调用函数时候确定的,所以,调用位置+调用对象确定this.