一文搞定闭包作用域this指向

本章概要

本来只想讲闭包,但是要把闭包真的弄清楚.牵扯的东西比较多,所以想放在一起.通过本章,你将会弄明白 作用域 作用域链 词法作用域 执行上下文 this指向 闭包 等js基础知识

作用域和作用域链

作用域的概念

作用域查就是变量的规则,相当于将变量包裹,隔离起来.域外不能访问域内.

js中有哪些作用域

  1. 全局作用域
  2. 函数作用域
  3. 块级作用域(Es6新增)

作用域链

多个作用域嵌套后,就有层级结构.如果在当前作用域不能找到变量,就会沿着外层作用域一直往外找,直到找到最外层.这种类似链条的关系就是我理解的作用域链.

变量提升

在a,b声明前打印a,b结果如下

js 复制代码
console.log(b,a) // f b(){} undefined
function b(){
    console.log('ji')
}
var a = 2
  1. var声明的变量以及非表达式声明的函数,编译器会在当前作用域顶部,创建同名变量
  2. 函数赋值为与变量同名的空函数体,否则为undefined
  3. 函数与变量同名, 函数优先级高
  4. 如果重复声明,后者覆盖前者

执行阶段

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.简单总结如下:

  1. 创建全局上下文
  2. 创建global对象指向window
  3. 压入栈底

执行上下文

上面已经对 全局上下文 简单的介绍,保存js代码在执行时需要用的信息一种抽象概念.除了全局上下文,还有 函数上下文 , eval上下文.eval上下文可忽略,官方并不推荐使用.

函数上下文

执行函数前准备阶段.

  1. 绑定this
  2. 创建变量对象VO,存储函数内部的变量
  3. 确定作用域链

执行函数

  1. 将函数上下文入栈
  2. 如果内部还有函数执行,将内部函数入栈,递归
  3. 栈内从顶部依次执行出栈
  4. 直到调用栈为空,整个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内部原理

  1. 闭包是编译器预编译导致的
  2. this是调用函数时候确定的,所以,调用位置+调用对象确定this.
相关推荐
徊忆羽菲18 分钟前
Echarts3D柱状图-圆柱体-文字在柱体上垂直显示的实现方法
javascript·ecmascript·echarts
轻语呢喃28 分钟前
JavaScript :字符串模板——优雅编程的基石
前端·javascript·后端
coding随想40 分钟前
JavaScript中的BOM:Window对象全解析
开发语言·javascript·ecmascript
難釋懷41 分钟前
TypeScript-webpack
javascript·webpack·typescript
Rockson1 小时前
使用Ruby接入实时行情API教程
javascript·python
前端小巷子2 小时前
Web开发中的文件上传
前端·javascript·面试
上单带刀不带妹3 小时前
手写 Vue 中虚拟 DOM 到真实 DOM 的完整过程
开发语言·前端·javascript·vue.js·前端框架
前端风云志3 小时前
typescript结构化类型应用两例
javascript
gnip4 小时前
总结一期正则表达式
javascript·正则表达式
爱分享的程序员4 小时前
前端面试专栏-算法篇:18. 查找算法(二分查找、哈希查找)
前端·javascript·node.js