JavaScript 原理篇

165 阅读16分钟

JS运行过程

1.js的运行过程可以简单的分为:

(1)解析:①词法分析(生成token);②语法分析(生成ast语法树)

(2)编译:①解释与编译

(3)执行:①执行上下文;②变量提升;③代码执行;④事件循环

(4)内存管理:①垃圾回收

我们主要讨论执行和内存管理这两部分

2.执行

(1)执行上下文:码的运行需要依赖一个运行环境,而这个运行环境就叫做执行上下文

(2)js的执行上下文有三种:

①全局执行上下文:

这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象,在代码执行的过程中let和const标识的变量也会在这个执行环境中定义和赋值。一个程序只会有一个全局执行上下文。

②函数执行上下文:

每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。

③eval执行上下文 执行在 eval 函数内部的代码也会有它属于自己的执行上下文

(3)调用栈:

调用栈的作用是管理程序的执行过程

函数是一个特殊的对象,它存储着代码字符串,只有在调用栈中才会被执行,调用栈是一个先进后出的数据结构,用来存储正在执行的执行上下文,如果有执行上下文想要执行则需要把上下文添加到调用栈顶部,例如函数的嵌套调用A()执行时回会添加到调用栈中,A()函数里面又调用B()函数,B()函数会添加A()函数上面优先执行B()函数,等到B()函数执行完后就弹出调用栈,再继续执行A()函数,等到A()函数执行完毕后再弹出调用栈,即函数调用结束。对于全局执行上下文,则是程序一起动就马上加入到栈底执行,遇到需要调用的函数就加入到栈顶优先执行,全部执行完毕后再退出调用栈

(4)执行上下文的创建

①绑定this

在全局上下文中,this默认(浏览器环境)默认指向window对象,而函数执行上下文的this则会根据函数的调用方式进行绑定,如果它被一个对象调用,那么 this 会被设置成那个对象,this在调用时绑定,是运行时绑定的

②创建词法环境

词法环境是一种持有标识符—变量映射的结构。(这里的标识符指的是变量/函数的名字,而变量是对实际对象[包含函数类型对象]或原始数据的引用)。

现在,在词法环境的内部有两个组件:(1) 环境记录器和 (2) 一个外部环境的引用

  1. 环境记录器是存储变量和函数声明的实际位置。
  2. 外部环境的引用意味着它可以访问其父级词法环境(作用域),函数的scope属性就是外部环境的引用,外部环境的引用在创建函数对象时就绑定到函数对象中去,是编译时绑定的

词法环境有两种类型:

  • 全局环境(在全局执行上下文中)是没有外部环境引用的词法环境。全局环境的外部环境引用是 null。它拥有内建的 Object/Array/等、在环境记录器内的原型函数(关联全局对象,比如 window 对象)还有任何用户定义的全局变量,并且 this的值指向全局对象。
  • 函数环境中,函数内部用户定义的变量存储在环境记录器中。并且引用的外部环境可能是全局环境,或者任何包含此内部函数的外部函数。

环境记录器

  1. 声明式环境记录器存储变量、函数和参数。
  2. 对象环境记录器用来定义出现在全局上下文中的变量和函数的关系。

环境记录(Environment Record)的本质可以看作是一个对象, 它用于保存特定作用域内的变量、函数声明等信息。在 JavaScript 中,有几种不同类型的环境记录,包括:

  1. 对象环境记录(Object Environment Record) :基于 JavaScript 对象的环境记录,用于处理全局作用域和 with 语句中的作用域。这种环境记录将变量和函数声明保存在一个普通的 JavaScript 对象中。
  2. 声明式环境记录(Declarative Environment Record) :用于处理函数作用域、块级作用域等,它直接存储变量和函数声明的标识符及其绑定值。
  3. 函数环境记录(Function Environment Record) :特殊的声明式环境记录,用于保存函数的形参、内部变量和内部函数声明。

这些环境记录实际上都是对象,它们可以包含键值对,其中键是变量名或函数名,值是相应的变量值或函数引用。这些环境记录对象都是 JavaScript 引擎在执行上下文中动态创建的,用于管理当前作用域内的变量和函数声明。

(5)内存管理

更新中...

(6)参考文献

[译] 理解 JavaScript 中的执行上下文和执行栈 - 掘金 (juejin.cn)

JS代码执行过程详解(面试中的加分项) - 掘金 (juejin.cn)

JavaScript 内存机制(前端同学进阶必备) - 掘金 (juejin.cn)

JS原型机制

image.png 1.什么是原型机制: js的原型是用于节省内存空间,方便共享属性管理的一种机制

2.原型机制的理解: js语言在是实现的时候内置了很多对象和函数,例如:Function、Object等构造函数,这些内置的构造函数在js代码执行时就会产生,并且会自带一个原型对象,js语言的机制会在这些原型对象上存放一些特定的内置属性,在通过这些内置构造函数new对象是时候,new的对象的原型对象就会指向这个内置构造函数的原型对象,这样一来new的对象就可以通过原型链找到这些属性并使用了,特别的是通过Function构造函数创建的函数对象都会有prototype属性,这个属性会指向一个空的对象,即函数的显示原型对象,函数的隐式原型对象则通过__proto__属性查找,函数的形式原型指向Function.prototype,而所有的普通对象(包括原型对象)都是通过Object构造函数创建,创建的对象的原型指向Object.prototype

3 .Object和Function的原型关系比较复杂,存在死锁的问题,对于 JavaScript 中 Function 和 Object 的循环依赖问题,实际上是一个比较复杂的情况,因为它涉及到 JavaScript 引擎的内部机制和规范定义。在大多数 JavaScript 引擎中,Function 和 Object 之间的循环依赖是通过一种特殊的方式来处理的。

在 JavaScript 中,Function 是一个特殊的对象,它也有自己的原型对象。而 Object 是一个构造函数,用于创建普通的对象。Function 和 Object 之间的循环依赖是这样处理的:

(1) Function 的创建:在 JavaScript 引擎启动时,会首先创建一个全局的 Function 对象。这个全局 Function 对象会在引擎初始化阶段就创建好,并且在引擎的整个生命周期中一直存在。

(2) Object 的创建:在 JavaScript 引擎初始化阶段,会创建一个全局的 Object 对象。这个全局 Object 对象也会在引擎初始化阶段就创建好,并且在引擎的整个生命周期中一直存在。

(3) Function 和 Object 的原型链:在创建 Function 对象和 Object 对象时,它们的原型链是同时建立的。具体来说,Function 对象的原型链会指向 Object 对象的原型对象,而 Object 对象的原型链会指向 Function 对象的原型对象。这样就形成了一个闭环的原型链。

虽然 Function 和 Object 之间存在循环依赖,但 JavaScript 引擎会通过特殊的方式来处理这种情况,确保它们的创建顺利进行,并且能够正确地使用原型链。JavaScript 引擎会在初始化阶段就创建好这两个对象,并确保它们的原型链正确地建立,以便在运行时能够正常地使用。

4.如图所示,原型链的链接关系:

image.png

5.原型机制的核心: 通过Object创建的的对象会有一个__proto__指向构造函数的原型对象,而Function可以理解为特殊的Object因为函数本身是特殊的对象,每个函数在通过Function函数创建的时候,会同时通过Object创建一个函数自己的原型对象,通过prototype指向这个函数自己的原型对象,通过__proto__指向构造函数的原型对象,至于Object和Function的原型关系可以理解为创世神,需要js引擎自己处理原型关系。

6.参考文献

这可能是掘金讲「原型链」,讲的最好最通俗易懂的了,附练习题! - 掘金 (juejin.cn)

JS异步编程

1.异步是指在一个时间点(并行)或时间段内(并发)同时干多件事,例如浏览器可以发送两个网络请求,获取api数据,这两个网络请求是同时进行的,这就是两个异步事件。

2.JavaScript 是一种单线程语言,这意味着在同一时间内,JavaScript 只能执行一个任务,即代码的执行是按顺序进行的,而不是同时执行多个段代码,JavaScript为了实现异步编程,引入了事件循环机制,JavaScript引擎提供了相关的异步api,如网络请求、文件io、dom事件等,JavaScript执行这些异步api后会通知JavaScript引擎执行相关操作,同时注册处理函数,操作执行完毕后会把结果和注册的函数包装成一个任务添加到任务队列,同步代码执行完成后JavaScript引擎会循环取出任务队列(宏任务和微任务)里面的任务到执行,执行时会把操作的执行结果以实参的形式传递给注册的处理函数,需要注意的是,异步操作完成时运行的处理函数是同步的,因为JavaScript代码只能运行在有一个线程

3.回调函数实现异步操作的同步执行,如果浏览器想要等待网络请求获取到数据后在执行后续的代码,可以将后续需要执行的代码封装成函数,把这个函数以参数的形式传入到异步api中,等到异步操作完成在我调用这个传入的函数,这样实现了异步操作的同步执行

4.Promise实现异步编程,Promise是一个构造函数,在创建promise对象的时候需要传入一个函数,这个函数在promise创建时会马上执行,个函数需要有两个参数,resolve和reject,这两个参数是Promise内部实现的函数,用于更改promise对象的状态,同时执行对象通过then函数向promise内部注册的回调函数,从而实现异步操作的同步执行

5.Promise的实现原理:核心思想是使用发布订阅模式通过then函数向promsie对象注册订阅函数,使用resolve和reject发布消息执行函数从而实现函数的同步调用,then函数需要返回一个promise2对象,这个promise2对象的状态需要根据前面一个promise对象的状态以及前面一个promise对象resolve传入实参的状态(如果这个实参是Promise类型的话)来决定是否要执行当前then返回的promise2的resolve函数

class MyPromise {
  constructor(execute) {
    this.status = "pending";
    this.value = undefined;
    this.reason = undefined;

    // 注册回调函数,如果promise对象状态改变执行注册的函数
    this.onSuccessCallBacks = [];
    this.onFailCallBacks = [];

    // 成功,改变状态并执行回调函数
    const resolve = (value) => {
      if (this.status !== "pending") return;
      this.status = "resolve";
      this.value = value;
      this.onSuccessCallBacks.forEach((fn) => {
        fn(value);
      });
    };

    // 失败,改变状态并执行回调函数
    const reject = (reason) => {
      if (this.status !== "pending") return;
      this.status = "reject";
      this.reason = reason;
      this.onFailCallBacks.forEach((fn) => {
        fn(reason);
      });
    };

    try {
      execute(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }

  // 注册回调函数并接收返回值
  then(onSuccessCallBack, onFailCallBack) {
    const that = this;
    if (
      typeof onSuccessCallBack !== "function" ||
      typeof onFailCallBack !== "function"
    )
      return console.log("then()必须传入函数");

    const promise2 = new MyPromise((resolve, reject) => {
      const successFn = (value) => {
        try {
          const x = onSuccessCallBack(value);
          that.resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      };

      const failFn = (value) => {
        try {
          const x = onFailCallBack(value);
          that.resolvePromise(promise2, x, resolve, reject);
        } catch (e) {
          reject(e);
        }
      };

      if (that.status == "resolve") {
        that.onSuccessCallBacks.push((value) => {
          setTimeout(() => {
            successFn(value);
          }, 0);
        });
      }

      if (that.status == "reject") {
        that.onSuccessCallBacks.push((reason) => {
          setTimeout(() => {
            failFn(reason);
          }, 0);
        });
      }

      if (that.status == "pending") {
        that.onSuccessCallBacks.push((res) => {
          setTimeout(() => {
            successFn(res);
          }, 0);
        });

        that.onFailCallBacks.push((res) => {
          setTimeout(() => {
            failFn(res);
          }, 0);
        });
      }
    });

    return promise2;
  }

  //核心:解析promise2(这步把我cpu干烧了)
  resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
      // 如果从onFulfilled中返回的x 就是promise2 就会导致循环引用报错
      return reject(new TypeError("循环引用"));
    }

    // 如果返回值是MyPromise类型的结果,则promise2继续等待返回的promise(x)执行完成后再执行
    if (x instanceof MyPromise) {
      if (x.status == "pending") {
        // x是promise对象,获取promise对象的then函数,向x对象注册回调函数,等待x对象的状态改变再改变promise2的状态
        const then = x.then;
        then.call(
          x,
          (y) => {
            // y为x对象的resolve接收的值,把这个值作为返回值重新赋值给promise2的resolvePromise
            this.resolvePromise(promise2, y, resolve, reject);
          },
          (reason) => {
            reject(reason);
          }
        );
      } else {
        resolve(x);
      }
    } else {
      resolve(x);
    }
  }
}

// 执行结果:立即打印1,一秒后打印then1: 2,再过一秒后打印then2: 3,实现then的链式调用
new MyPromise((resolve, reject) => {
  console.log(1);
  setTimeout(() => {
    resolve(2);
  }, 1000);
})
  .then(
    (res) => {
      console.log("then1:", res);
      return new MyPromise((resolve, reject) => {
        setTimeout(() => {
          resolve(3);
        }, 1000);
      });
    },
    (reason) => {
      console.log(reason);
    }
  )
  .then(
    (res) => {
      console.log("then2:", res);
    },
    (reason) => {
      console.log(reason);
    }
  );

如图所示,then函数返回的promise2的状态会依赖于前面一个promise的状态,这是then函数在向前面一个promise对象注册回调函数的时候,会同时把then函数新创建的promise2的resolve2和reject2同时传进去,等到前面一个promise执行resolve或reject时,如果注册的回调函数返回值不是promise的话,会同时执行resolve2或reject2函数,如果回调函数返回值是promise的话,则继续把resolve2和reject2传入到这个promise,等到这个promise状态改变再执行,进而改变promise2的状态

image.png

6.参考资料

Promise详解与实现(Promise/A+规范) - 简书 (jianshu.com)

Promise.all的实现原理

const p1 = function(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
         resolve()   
        }, 1000)
    })
}

const p2 = function(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
         resolve(2)   
        }, 2000)
    })
}

const p3 = function(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
         resolve(3)   
        }, 3000)
    })
}

const all = function(promises) {
    if(!Array.isArray(promises)){
        throw new TypeError('传入的参数必须是数组类型')
    }

    let resolveCount = []
    const results = []

    return new Promise((resolve, reject) => {
        promises.forEach(async (item, index) => {
            const res = await Promise.resolve(item)
            results[index] = res
            resolveCount++
            if(resolveCount == promises.length){
                resolve(results)
            }
        })
    }).catch((e) => {
        reject(e)
    })
}

const pAll = all([1, p2(), p3()])

pAll.then(res => {
    console.log(res);
})

// 三秒后打印[1,2,3]

Promise.allSettled的实现原理

const p1 = function(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
         resolve(1)   
        }, 1000)
    })
}

const p2 = function(){
    return new Promise((resolve, reject) => {
        setTimeout(() => {
         reject(new Error('p2'))   
        }, 1000)
    })
}

const allSettled = function(promiseArr){
    if(!Array.isArray(promiseArr)){
        throw new TypeError('必须传入数组')
    }

    let settledCount = 0
    const results = []

    return new Promise((resolve, reject) => {
        promiseArr.forEach(async (item, index) => {
            Promise.resolve(item).then((value) => {
                results[index] = value
            }, (reason) => {
                results[index] = reason
            }).finally(() => {
                settledCount++
                if(settledCount == promiseArr.length){
                    resolve(results)
                }
            })
        })
    })
}

allSettled([p1(), p2(), 3]).then(res => {
    console.log(res); // 打印结果[1, 报错p2, 3]
})

数组foreach方法的实现原理

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];

const myForEach = function (callback) {
  let length = this.length;
  if (typeof callback != "function") {
    throw new TypeError("传入的参数不是函数");
  }

  for (let i = 0; i < length; i++) {
    callback.call(this, this[i], i);
  }
};

myForEach.call(arr, (item, index) => {
  console.log(item, index);
});

数组reduce方法的实现原理

const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];

const myReduce = function (callback, initValue) {

  if (!(this instanceof Array)) {
    throw new TypeError("只有数组可以调用该函数");
  }

  if (typeof callback != "function") {
    throw new TypeError("传入的第一个参数必须是函数");
  }

  if (initValue == undefined) {
    throw new TypeError("初始值不能为undefined");
  }

  let sum = initValue;
  const length = arr.length;
  let startIndex = 0;

  for (let i = startIndex; i < length; i++) {
    if (i in this) {
      sum = callback.call(undefined, sum, this[i], i);
    }
  }

  return sum;
};

const sum = myReduce.call(
  arr,
  (sum, cur) => {
    return sum + cur;
  },
  0
);

console.log(sum); // 输出45

async/await的实现原理

1.async/await简介: async关键字用于标记异步函数,被标记的函数返回值是一个promise对象,await关键字只能在异步函数内部使用,用于等待一个 Promise 对象的状态变更,当 Promise 对象状态变为 resolved(已解决)时,await 表达式返回 Promise 的解决值;如果 Promise 的状态变为 rejected(已拒绝),则会抛出一个错误,可以通过 try/catch 来捕获。

2.通过generator和prromise实现async/await关键字:

这里我们做了一个简单的实现没有考虑yield返回非promise的情况,假设yield返回的全部是promise,实现的核心思想是,把获取下一个promise的操作放到前面一个promise的then函数作为回调函数执行,等到前面一个promise执行resolve或reject再获取下一个promise,利用递归重复这样的操作

const p1 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(1)
    }, 1000)
  })
}

const p2 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(2)
    }, 2000)
  })
}

const p3 = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(3)
    }, 3000)
  })
}

function* generator(){
  const res1 = yield p1()
  console.log(res1);
  const res2 = yield p2()
  console.log(res2);
  const res3 = yield p3()
  console.log(res3);
  return 4
}

const asyncFn = () => {
  const gen = generator()
  function step(res) {
    const { value, done } = gen.next(res)
    if(!done){
      value.then(res => {
        step(res)
      })
    }
  }
  step(null)
} 

asyncFn()

3.参考文献

手写async await的最简实现(20行) - 掘金 (juejin.cn)

new关键字的实现原理

const newFn = function(constructor, ...args){
    const obj = {}

    Object.setPrototypeOf(obj, constructor.prototype)

    const result = constructor.call(obj, ...args)

    return result instanceof Object? result : obj
}

function Person(name){
    this.name = name
}

Person.prototype.sayName = function(){
    console.log(this.name);
}

const person = newFn(Person, '张三')

person.sayName() // 打印张三
console.log(person); // 打印 { name: '张三' }

call方法的实现原理

const obj = {
  name: "张三",
  sex: "男",
  age: 22,
};

const sayHi = function () {
  console.log("Hi,", this.name);
};

const myCall = function (context, ...args) {
  console.log("args:", args);
  context = context || window;
  context.tempFunc = this; // this指向的是函数对象
  const res = context.tempFunc(...args);
  delete context.tempFunc;
  return res;
};

Function.prototype.myCall = myCall;

sayHi.myCall(obj, "xxx");
const obj = {
  name: "张三",
  sex: "男",
  age: 22,
};

const sayHi = function () {
  console.log("Hi,", this.name);
};

const myCall = function (context, ...args) {
  console.log("args:", args);
  context = context || window;
  context.tempFunc = this; // this指向的是函数对象
  const res = context.tempFunc(...args);
  delete context.tempFunc;
  return res;
};

Function.prototype.myCall = myCall;

sayHi.myCall(obj, "xxx");

instenceof的实现原理

function myInstance(obj, construct){

  let proto = Object.getPrototypeOf(obj)

  while(proto != null){
    if(proto == construct.prototype){
      return true
    }
    proto = Object.getPrototypeOf(proto)
  }
  return false
}

function Person1(name, age){
  this.name = name
  this.age = age
}

function Person2(name, age){
  this.name = name
  this.age = age
}

const person = new Person1('张三', 22)

console.log(myInstance(person, Person1)); // true
console.log(myInstance(person, Person2)); // false

Object、Reflect、definedProperty、Proxy的详解

更新中...