Skip to content
On this page

前端内参

链接

JavaScript

作用域、作用域链

作用域:即变量和函数的可访问区域,是静态的,分为全局作用域和局部作用域,由代码的书写位置决定的,局部作用域也叫静态作用域/词法作用域,在函数定义的时候就已经确定了,而不是在函数调用的时候确定的。

作用域链:多个作用域对应的变量对象串联起来组成的链表就是作用域链,这个链表是以引用的形式保持对变量对象的访问。

执行上下文

当前 JavaScript 代码被解析和执行时所在的环境。也叫做执行环境。

  • 全局执行上下文在代码开始执行时就创建,有且只有一个,永远在执行上下文栈的最底部,浏览器窗口关闭时它才出栈。
  • 函数被调用时创建函数的执行上下文环境,并且入栈。
  • 只有栈顶的执行上下文才是处于活动状态的,也即只有栈顶的变量对象才会变成活动对象。
  • 生命周期:
    1. Step1 创建阶段:
      1. 创建函数变量对象,初始化函数参数 arguments,提升函数和变量的声明;
      2. 创建作用域链,作用域链是在变量对象之后创建的,作用域链包含变量对象。作用域链用于解析变量。解析变量时,会沿着作用域链向上查找,直到找到为止;
      3. 确定 this 指向。
    2. Step2 执行阶段:开始执行代码,在这个过程会完成变量赋值、函数引用、执行其他代码。
    3. Step3 回收阶段:调用完毕后,函数出栈,对应的执行上下文也出栈,等待垃圾回收器回收执行上下文。

执行上下文生命周期

变量对象的创建规则:

  1. 建立 arguments 对象,检查执行上下文参数,建立该对象下的属性与属性值;
  2. 检查当前上下文中的函数声明,也就是 function 关键字声明函数,在变量对象中以函数名创建一个属性,属性值指向该函数所在内存地址的引用,如果已经存在该属性,那么该属性会被新的引用覆盖;
  3. 检查当前上下文中的变量声明,每找到一个变量声明,就会在变量对象中以该改变量名建立一个属性,属性值为 undefined,如果该变量名的属性已存在就会跳过,以防止同名的函数被修改。

变量对象创建过程

彻底搞懂 this

JavaScript 中的 this 指向函数的调用位置的对象,也即调用该函数的对象。this 的指向是在函数调用时确定的,而不是在函数定义时确定的。 在 setTimeoutsetInterval 中,this 指向 window

  1. this 指向实在函数调用时确定的,也就是执行上下文被创建时;
  2. this 指向与函数声明位置没有任何关系,只取决于函数的调用位置(也即由谁、在什么地方调用的该函数);
  3. 因为在执行上下文创建阶段 this 指向已经被确定,在执行阶段 this 指向不可再被更改;
  4. this 指向最靠近被调用函数的对象。
js
function func() {
  console.log(this.a)
}

var obj2 = {
  a: '1891',
  func: func,
}

var obj1 = {
  a: 'coffe',
  obj2: obj2,
}

//此时的 this 指向 obj2 对象,因为obj2离得近!
obj1.obj2.func() //>> 1891

new 关键字

  1. 创建一个空的简单的 JavaScript 对象(即 {} );
  2. 为步骤 1 新创建的对象添加属性 __proto__,将该属性指向构造函数的原型对象;
  3. 将步骤 1 新创建的对象作为 this 的上下文;
  4. 如果该函数没有返回对象,则返回 this

callapplybind

  1. callapplybind 都是用来改变函数的 this 对象的指向的;
  2. call/apply:返回函数执行结果,bind:返回绑定后的函数;

手写 callapplybind
call

js
Function.prototype.myCall = function (thisArg, ...arr) {
  //1.判断参数合法性/////////////////////////
  if (thisArg === null || thisArg === undefined) {
    //指定为 null 和 undefined 的 this 值会自动指向全局对象(浏览器中为window)
    thisArg = window
  } else {
    thisArg = Object(thisArg) //创建一个可包含数字/字符串/布尔值的对象,
    //thisArg 会指向一个包含该原始值的对象。
  }

  //2.搞定this的指向/////////////////////////
  const specialMethod = Symbol('anything') //创建一个不重复的常量
  //如果调用myCall的函数名是func,也即以func.myCall()形式调用;
  //根据上篇文章介绍,则myCall函数体内的this指向func
  thisArg[specialMethod] = this //给thisArg对象建一个临时属性来储存this(也即func函数)
  //进一步地,根据上篇文章介绍,func作为thisArg对象的一个方法被调用,那么func中的this便
  //指向thisArg对象。由此,巧妙地完成将this隐式地指向到thisArg!
  let result = thisArg[specialMethod](...arr)

  //3.收尾
  delete thisArg[specialMethod] //删除临时方法
  return result //返回临时方法的执行结果
}

apply

js
Function.prototype.myApply = function (thisArg) {
  if (thisArg === null || thisArg === undefined) {
    thisArg = window
  } else {
    thisArg = Object(thisArg)
  }

  //判断是否为【类数组对象】
  function isArrayLike(o) {
    if (
      o && // o不是null、undefined等
      typeof o === 'object' && // o是对象
      isFinite(o.length) && // o.length是有限数值
      o.length >= 0 && // o.length为非负值
      o.length === Math.floor(o.length) && // o.length是整数
      o.length < 4294967296
    )
      // o.length < 2^32
      return true
    else return false
  }

  const specialMethod = Symbol('anything')
  thisArg[specialMethod] = this

  let args = arguments[1] // 获取参数数组
  let result

  // 处理传进来的第二个参数
  if (args) {
    // 是否传递第二个参数
    if (!Array.isArray(args) && !isArrayLike(args)) {
      throw new TypeError('第二个参数既不为数组,也不为类数组对象。抛出错误')
    } else {
      args = Array.from(args) // 转为数组
      result = thisArg[specialMethod](...args) // 执行函数并展开数组,传递函数参数
    }
  } else {
    result = thisArg[specialMethod]()
  }

  delete thisArg[specialMethod]
  return result // 返回函数执行结果
}

bind

js
/**
 * 用原生JavaScript实现bind
 */
Function.prototype.myBind = function (objThis, ...params) {
  const thisFn = this //存储调用函数,以及上方的params(函数参数)
  //对返回的函数 secondParams 二次传参
  let funcForBind = function (...secondParams) {
    //检查this是否是funcForBind的实例?也就是检查funcForBind是否通过new调用
    const isNew = this instanceof funcForBind

    //new调用就绑定到this上,否则就绑定到传入的objThis上
    const thisArg = isNew ? this : Object(objThis)

    //用call执行调用函数,绑定this的指向,并传递参数。返回执行结果
    return thisFn.call(thisArg, ...params, ...secondParams)
  }

  //复制调用函数的prototype给funcForBind
  funcForBind.prototype = Object.create(thisFn.prototype)
  return funcForBind //返回拷贝的函数
}

闭包 Closure

内层的作用域访问它外层函数作用域里的参数/变量/函数时,闭包就产生了。
闭包可以让开发者从内部函数访问外部函数的作用域,闭包会随着函数的创建而被同时创建。

原型和原型链

  1. 原型是一个对象;
  2. prototype 是函数的一个属性而已,只有函数才有 prototype 属性;
  3. 每个对象(实例)都有一个属性 __proto__,指到它的构造函数(constructor)的 prototype 属性。
  4. 一个对象的原型就是它构造函数的prototype属性,也就是 __proto__ 指向的对象。
  5. 对象的__proto也有自己的__proto__,层层向上,直到 null

不推荐使用 __proto__,推荐使用 Object.getPrototypeOf()。设置 __proto__ 会导致性能问题,建议使用 Object.create()

js
const obj = {
  name: 'obj',
  getName() {
    return this.name
  },
}

const obj2 = Object.create(obj) // obj2 === {__proto__: obj} 以obj为原型创建一个新对象
js
class A {
  constructor() {}
  toString() {}
  toValue() {}
}
// 等同于
A.prototype = {
  constructor() {},
  toString() {},
  toValue() {},
}

类的所有方法都定义在类的 prototype 属性上面。

同步和异步,阻塞和非阻塞

同步和异步其实指的是,请求发起方对消息结果的获取是主动发起的,还是被动等通知的。 同步阻塞和同步非阻塞都需要发起方直动获取消息结果,只是获取结果的方式不同。非阻塞要通过不断轮询的方式来获取结果。

如果请求是由服务方通知的,也就是请求方发出请求后,要么在一直等待通知(异步阻塞),要么就先去干自己的事(异步非阻塞)。当事情处理完成之后,服务方会主动通知请求方,他的请求已经完成了,这就是异步,异步通知的方式一般是通过状态改变,消息通知,或者回调函数来完成,大多数时候采用的都是回调函数。

对象同步阻塞同步非阻塞异步阻塞异步非阻塞
请求方主动获取主动获取被动等待被动等待
服务方被动等待被动等待主动通知主动通知

image.png 阻塞和非阻塞指的是,请求发起方对消息结果的获取是等待结果返回后再继续执行,还是不等待结果返回就继续执行。

Event Loop

JS 引擎是单线程的,但是浏览器是多线程的,所以 JS 引擎需要通过 Event Loop 来实现异步。

浏览器线程包括:

  1. GUI 渲染线程
  2. JS 引擎线程
  3. 事件触发线程(onclick 等)
  4. 定时触发器线程
  5. 异步 http 请求线程

GUI 渲染线程和 JS 引擎线程是互斥的,虽然两者属于不同的线程,但是由于 JS 执行结果可能会对页面产生影响,所以浏览器对此做了限制,只有当 JS 引擎空闲时,GUI 线程才能被激活。

MacroTask(宏任务):包括整体代码 script,setTimeout,setInterval,I/O,UI 渲染等。

MicroTask(微任务):Promise,process.nextTick,Object.observe,MutationObserver。

页面重排(Reflow)和重绘(Repaint)

重排:当 DOM 的变化影响了元素的几何信息(尺寸,位置),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排,也叫回流,简单地说就是重新生成布局,重新排列元素。

那些情况会导致重排:

  1. 页面初始渲染,这是开销最大的一次重排
  2. 添加/删除可见的 DOM 元素
  3. 改变元素位置
  4. 改变元素尺寸,如边距、填充、边框、宽高等
  5. 改变元素内容,如文本或图片等
  6. 改变元素字体大小
  7. 改变浏览器窗口大小,如 resize
  8. 激活 CSS 伪类,如::hover
  9. 设置 Style 属性的值,因为通过设置 Style 属性改变节点样式的话,每一次设置都会触发一次 reflow

重排影响范围: 浏览器渲染界面是基于流式布局模型的,所以触发重排时会对周围 DOM 重新排列,影响的范围有两种:全局范围:从根节点 html 开始对整个渲染树进行重新布局。局部范围:对渲染树的某部分或某一个渲染对象进行重新布局。

重排的代价是高昂的,应该尽可能减少重排的次数,减少重排的范围。 优化重排的方法:

  1. 减少范围:

    • 尽可能在底层级的 DOM 节点上修改样式,避免影响到上层节点
    • 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
  2. 减少次数

    • 样式集中改变
    • 分离读写操作,避免多次触发重排
    • 将 DOM 离线后修改,修改完成后再添加到文档中,使用 documentFragment 在内存中修改 DOM,修改完成后再添加到文档中
    • 使用 absolute 或 fixed 定位脱离文档流,避免影响到其他元素
    • 优化动画

重绘(Repaint)

当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程叫做重绘。