let/const块级作用域是怎样实现的?闭包是怎样实现的?this有什么设计缺陷?

305 阅读4分钟

Javascript代码的执行

本文介绍了块级作用域的在内存上的实现与闭包的实现,在此之前需要简单了解一下JavaScript代码的执行.

编译

经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码

执行上下文

变量环境词法环境构成;分为全局执行上下文和函数执行上下文;所有的执行上下文又构成执行栈.

  1. 词法环境:通过let/const声明的変量存在于词法环境,即块即作用域;代码块內部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁

    在词法环境内部,维护了一个小型栈结构 函数内let和var声明同一个变量可以吗? 暂时性死区

    image.png

  2. 变量环境:通过var申明的变量和函数声明存在于此

    变量和函数同名时:变量的声明会被忽略;同名的函数,选择最后声明的那个

image.png

image.png

执行

执行Javascrip 会存在多个执行上下文;通过栈的数据结构来管理的,执行上下文栈,又称调用栈。

执行栈

全局执行上下文,只有一份;函数执行上下文,函数执行结束之后,会被销毁

查找变量

  1. 从词法环境的栈顶向下查
  2. 变量环境中查找
  3. 在外部环境中查找

变量查找是静态的过程——编译阶段就决定,与调用没有关系

image.png

外部环境

Screen Shot 2021-04-21 at 4.04.38 PM.png

打印出来是极客邦!!因为一个对象(bar)不是一个执行环境,只有全局执行环境和函数执行环境,在执行13行时,在当前的执行环境没有找到变量,会开始寻找外部环境,这个外部环境是全局执行环境,不是bar!!!

在当前执行环境中没有找到的变量,会向外部环境去查找

外部环境:当前的函数定义的环境,和调用无关

image.png

闭包:外函数返回的内函数对外函数中的变量有引用

外部函数已经执行结束

这些变量依然保存在内存,称为外函数的闭包

内存泄漏:被返回的内函数是一个全局变量,那么闭包会一直存在直到页面关闭

 如何避免内存泄露:在函数作用域而不是全局作用域调用有用包的函数 等函数销毁后就会回收这块内存

闭包执行情况

image.png image.png

对象查找

在上面这个例子中,打印出来的结果是不合常理的,在其他语言中,应该打印出来的是函数所属的对象中的变量

class Bar {
  name = "brynn";
  getName() {
    console.log(name);
  }
}
let name = "global environment";
let bar = new Bar();
bar.getName();

"global environment"

比如用c++ch;en inp来复写这段代码

#include <iostream>
using namespace std;
class Bar {
  public:
  char* name;
  Bar(){
    name = (char*)"brynn";
  }
  void getName() {
    cout<<name<<endl;
  }
} bar ;
char* name = (char*)"gloabal environment";
int main() {
  bar.getName();
  return 0;
}

"brynn"

所以引入了this

指向:指向调用方法的对象本身/指向window对象(在严格模式下this值是 undefined)

设置函数执行上下文中的this指向 image.png

image.png

bar{myname:'极客时间',test1;1}
mtObj{}
myObj{}

手写call/apply/bind

  • call

testFun.call(alterObj, arg1, arg2, ...)

Function.prototype.myCall = function (alterObj, ...args) {
  alterObj = alterObj || (typeof window === "undefined" ? global : window);
  // originalObj.testFun.myCall(alterObj, 'argue1', 'argue2');this指向testFun
  alterObj.tempFun = this; // 在对象上添加方法
  alterObj.tempFun(...args); // 调用方法
  delete alterObj.tempFun; // 删除方法
};
  • apply

testFun.apply(alterObj, [arg1, arg2, ...])

注意一下展开运算符...:“args”是一个数组,“...args”是数组是的所有元素,“args和[...argus]”代表都是一个数组

Function.prototype.myApply = function (alterObj, [...args]) {
  alterObj = alterObj || (typeof window === "undefined" ? global : window);
  alterObj.tempFun = this; 
  alterObj.tempFun(...args);
  delete alterObj.tempFun;
};
  • bind

testFun.bind(alterObj, arg1, arg2, ...)

Function.prototype.myBind = function (alterObj, ...args) {
  alterObj = alterObj || (typeof window === "undefined" ? global : window);
  let self = this;
  // this指向testFun
  return function () {
    // 这里面的bind指向alterFun 
    let addAguments = arguments;
    self.call(alterObj, ...args, ...addAguments);
  };
};

let originalObj = {
  name: "originalBrynn",
  testFun: function (m, n) {
    console.log(this);
    console.log(m, n);
  }
};
let alterObj = {
  name: "alterBrynn"
};
originalObj.testFun("argue1", "argue2");
let alterFun = originalObj.testFun.myBind(alterObj, "argue1");
alterFun("argue2");
this设计缺陷
  1. 嵌套函数中的this不会从外层函数中继承,永远是指向最近的调用对象

每一个执行上下文都会创建一个this(即每一个普通函数以及全局执行环境都创建了一个this,由于任何变量的查找都是通过一样的流程,会首先找到最近的this)

let bar = {
  name: "brynn",
  outerFun: function () {
    let self = this;
    console.log("外层函数中this指向bar对象", this);
    function innerFun() {
      console.log("内层函数中this指向全局window/undefined", this);
      console.log("用self可以访问同一个对象", self);
    }
    innerFun();
  }
};
bar.outerFun();

解决方法:

1. 声明一个変量self 用来保存this

2. 也可以使用ES6中的箭头函数

2. setTimeout推迟执行不合预期

image.png