一文搞定闭包作用域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.
相关推荐
aPurpleBerry8 分钟前
JS常用数组方法 reduce filter find forEach
javascript
ZL不懂前端1 小时前
Content Security Policy (CSP)
前端·javascript·面试
乐闻x1 小时前
ESLint 使用教程(一):从零配置 ESLint
javascript·eslint
我血条子呢1 小时前
[Vue]防止路由重复跳转
前端·javascript·vue.js
半开半落2 小时前
nuxt3安装pinia报错500[vite-node] [ERR_LOAD_URL]问题解决
前端·javascript·vue.js·nuxt
理想不理想v2 小时前
vue经典前端面试题
前端·javascript·vue.js
小阮的学习笔记2 小时前
Vue3中使用LogicFlow实现简单流程图
javascript·vue.js·流程图
YBN娜2 小时前
Vue实现登录功能
前端·javascript·vue.js
阳光开朗大男孩 = ̄ω ̄=2 小时前
CSS——选择器、PxCook软件、盒子模型
前端·javascript·css