闭包 & 作用域

159 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

PS:已经更文多少天,N就写几。一定要写对文案,否则文章不计入在内

首先想先归纳名词:

  • 原型链
  • 作用域链
  • this 指向

前言

原型链 vs 作用域链

从名字上看,都是一种链表的结构,实际上略有不同

  1. 原型链是动态的,作用域链是静态的

    动态的意思是指,在运行过程中修改 __ proto __ 就可以修改 原型链的 细节

  2. 使用场景上:

    • 原型链是某一对象上的属性找不到,一路往上找
    • 作用域链是某一作用域里变量找不到,一路网上找

作用域 vs this

在 JS 中创建 对象 有两种方法

  1. 使用 new
  2. 自定义一个 createXX 的工厂方法返回

使用new创建的对象,往往会比较符合从JAVA来的同学的直觉,一个对象,然后 this 访问这个实例上的一些方法/属性;

function Person(name, age){
    this.name = name
    this.age = age
    this.time = 100
    this.isLive = function(){
        return this.time > 0
    }
}

class Person{
    constrator(name, age){
            this.name = name
            this.age = age
            this.time = 100
    }
    isLive(){
        return this.time > 0
    }
}

let p = new Person("xiaoming", 20);
p.isLive();

使用createXX的代码一般如下

function createPerson(name, age){
    let time = 100
    let __TIME = 99999//冗余的function isLive(){
        return time > 0;
    }

    return {
        name,
        age,
        isLive
    }
}

let p = createPerson("xiaoming", 20);
p.isLive();

代码的语义就是一个人,然后可以检查自己是否还活着。 在工厂方法中,isLive去访问time的时候,是随着作用域往上找了一层,返回一整个对象的时候使用闭包,把这个本应随着执行上下文一起销毁的变量存了下来;

加入这里有一个冗余的属性(__TIME),那么它就会销毁掉了;

而使用 class 的代码中,可以理解为这个“time”被认为是这个对象的一部分存了起来,如果有冗余的属性,也会一并存储

甚至可以再简单的区分:

  • createXX 访问time需要跨作用域
  • class 则不需要跨作用域

闭包

闭包允许函数访问并操作函数外部的变量。只要变量或者函数存在于声明函数时的作用域内,闭包既可使函数能够访问到这些变量或函数。

此处提到一个概念 “存在于声明函数时的作用域内”, 但是我们知道作用域其实是一个链条,我理解应该称之为“存在于声明函数时的作用域链上”

闭包最核心的观点就是:函数或者变量可以在调用栈弹出销毁的过程中被保留下来。

闭包最直观的体会就是:在某一函数作用域中声明一个函数的时候,如果这个内部函数中用到了外部函数的一些变量,就会有一个圈,把这些用到的变量都包裹起来,只要这个内部函数实例不销毁,那么这个圈里的变量也不会被清理。

闭包的作用

私有变量

回调函数

在C中,函数往往被理解成一个相对而言比较纯粹的方法:有输入然后输出,只要不涉及一些全局变量,一般都是一个纯函数;

例如要给“点击事件”

// 不用闭包:调整速度
function handleClick(animationContext){
    animationContext.speed = xx;
}

我们需要把一些参数用地址引用的方式传给这个方法,然后再去修改

let speed = 0;
function handleClick(){
    speed = xx
}

我们传递的回调函数可以理解成有状态的函数,哪怕是一样的输入,随着上下文的不同,他的输出是不一样的,闭包可以减少很多不必要的值传递

语法环境:执行上下文 & 调用栈

捋清楚执行上下文、调用栈、作用域之间的关系

  • 每个JS程序只有一个全局执行上下文,每有一个函数调用,就推入一个函数执行上下文

词法环境:作用域

词法环境一般就称之为作用域(scopes)

在ES6中作用域有以下几种

  1. 全局作用域
  2. 函数作用域
  3. 块级作用域
  4. Eval / with 作用域:不推荐~

💡 在要用到一个变量的时候,比如 console.log(a), 就进行词法环境的查询

主要关注的是块级作用域:因为在 ES6之前JS是没有这种作用域的, 之前es5识别不了块级作用域,所以声明变量什么的会绑定到全局作用域或者当前函数作用域上

💡 常见的面试题 var 就来自于此:声明的变量绑定到最近的函数/块级作用域上

无论何时调用函数,都会创建一个新的执行环境,被推入执行上下文栈。此外,还会创建一个与之相关联的词法环境。最重要的是:外部环境与新建的词法环境,JS引擎将调用函数的内置属性[[Environment]] 属性与创建函数时的环境进行关联

const、let、var

区分这三者的维度有两个

  1. 可变性

    • 语法上:const 动了会报错

    • 语义上:const 定义常量;const 来显示的定义一些不可变的数据

    在React中,我们往往会强调数据不可变性,但是用const定义的对象,里面的值还是可以改的,所以需要改写get,proxy 或者直接上第三方库

    • 底层:Js引擎可能会针对 const 进行优化
  2. 作用域

    使用 var 的时候,该变量是在距离最近的函数作用域或者全局作用域的词法环境中定义的

    let / const 还可以在块级作用域中定义

变量提升:注册标识符

变量和函数统称为标识符

Javascript 代码执行的两个阶段

  1. (如果是函数执行上下文,创建形参以及默认值)
  2. 访问并注册当前词法环境中所声明的变量和函数
  3. 执行代码
  • var 提升

    var 声明的变量提升到顶层后,后续访问是 undefined,直到该行代码执行才赋值

  • 函数提升

    函数提升后,后续访问就是这个对象,并且就算中途篡改了这个对象,执行到改行代码的时候,也不会纠正过来的

// 函数优先

console.log(t)   // function
function t(){};
var t = 1