前端内参
JavaScript
作用域、作用域链
作用域:即变量和函数的可访问区域,是静态的,分为全局作用域和局部作用域,由代码的书写位置决定的,局部作用域也叫静态作用域/词法作用域,在函数定义的时候就已经确定了,而不是在函数调用的时候确定的。
作用域链:多个作用域对应的变量对象串联起来组成的链表就是作用域链,这个链表是以引用的形式保持对变量对象的访问。
执行上下文
当前 JavaScript 代码被解析和执行时所在的环境。也叫做执行环境。
- 全局执行上下文在代码开始执行时就创建,有且只有一个,永远在执行上下文栈的最底部,浏览器窗口关闭时它才出栈。
- 函数被调用时创建函数的执行上下文环境,并且入栈。
- 只有栈顶的执行上下文才是处于活动状态的,也即只有栈顶的变量对象才会变成活动对象。
- 生命周期:
- Step1 创建阶段:
- 创建函数变量对象,初始化函数参数
arguments
,提升函数和变量的声明; - 创建作用域链,作用域链是在变量对象之后创建的,作用域链包含变量对象。作用域链用于解析变量。解析变量时,会沿着作用域链向上查找,直到找到为止;
- 确定
this
指向。
- 创建函数变量对象,初始化函数参数
- Step2 执行阶段:开始执行代码,在这个过程会完成变量赋值、函数引用、执行其他代码。
- Step3 回收阶段:调用完毕后,函数出栈,对应的执行上下文也出栈,等待垃圾回收器回收执行上下文。
- Step1 创建阶段:
变量对象的创建规则:
- 建立
arguments
对象,检查执行上下文参数,建立该对象下的属性与属性值; - 检查当前上下文中的函数声明,也就是
function
关键字声明函数,在变量对象中以函数名创建一个属性,属性值指向该函数所在内存地址的引用,如果已经存在该属性,那么该属性会被新的引用覆盖; - 检查当前上下文中的变量声明,每找到一个变量声明,就会在变量对象中以该改变量名建立一个属性,属性值为
undefined
,如果该变量名的属性已存在就会跳过,以防止同名的函数被修改。
彻底搞懂 this
JavaScript 中的 this
指向函数的调用位置的对象,也即调用该函数的对象。this
的指向是在函数调用时确定的,而不是在函数定义时确定的。 在 setTimeout
和 setInterval
中,this
指向 window
。
this
指向实在函数调用时确定的,也就是执行上下文被创建时;this
指向与函数声明位置没有任何关系,只取决于函数的调用位置(也即由谁、在什么地方调用的该函数);- 因为在执行上下文创建阶段
this
指向已经被确定,在执行阶段this
指向不可再被更改; this
指向最靠近被调用函数的对象。
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
关键字
- 创建一个空的简单的 JavaScript 对象(即
{}
); - 为步骤 1 新创建的对象添加属性
__proto__
,将该属性指向构造函数的原型对象; - 将步骤 1 新创建的对象作为
this
的上下文; - 如果该函数没有返回对象,则返回
this
。
call
、apply
、bind
call
、apply
、bind
都是用来改变函数的this
对象的指向的;call
/apply
:返回函数执行结果,bind
:返回绑定后的函数;
手写 call
、apply
、bind
:call
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
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
/**
* 用原生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
内层的作用域访问它外层函数作用域里的参数/变量/函数时,闭包就产生了。
闭包可以让开发者从内部函数访问外部函数的作用域,闭包会随着函数的创建而被同时创建。
原型和原型链
- 原型是一个对象;
prototype
是函数的一个属性而已,只有函数才有prototype
属性;- 每个对象(实例)都有一个属性
__proto__
,指到它的构造函数(constructor)的prototype
属性。 - 一个对象的原型就是它构造函数的
prototype
属性,也就是__proto__
指向的对象。 - 对象的
__proto
也有自己的__proto__
,层层向上,直到null
;
不推荐使用 __proto__
,推荐使用 Object.getPrototypeOf()
。设置 __proto__
会导致性能问题,建议使用 Object.create()
。
const obj = {
name: 'obj',
getName() {
return this.name
},
}
const obj2 = Object.create(obj) // obj2 === {__proto__: obj} 以obj为原型创建一个新对象
class A {
constructor() {}
toString() {}
toValue() {}
}
// 等同于
A.prototype = {
constructor() {},
toString() {},
toValue() {},
}
类的所有方法都定义在类的 prototype
属性上面。
同步和异步,阻塞和非阻塞
同步和异步其实指的是,请求发起方对消息结果的获取是主动发起的,还是被动等通知的。 同步阻塞和同步非阻塞都需要发起方直动获取消息结果,只是获取结果的方式不同。非阻塞要通过不断轮询的方式来获取结果。
如果请求是由服务方通知的,也就是请求方发出请求后,要么在一直等待通知(异步阻塞),要么就先去干自己的事(异步非阻塞)。当事情处理完成之后,服务方会主动通知请求方,他的请求已经完成了,这就是异步,异步通知的方式一般是通过状态改变,消息通知,或者回调函数来完成,大多数时候采用的都是回调函数。
对象 | 同步阻塞 | 同步非阻塞 | 异步阻塞 | 异步非阻塞 |
---|---|---|---|---|
请求方 | 主动获取 | 主动获取 | 被动等待 | 被动等待 |
服务方 | 被动等待 | 被动等待 | 主动通知 | 主动通知 |
阻塞和非阻塞指的是,请求发起方对消息结果的获取是等待结果返回后再继续执行,还是不等待结果返回就继续执行。
Event Loop
JS 引擎是单线程的,但是浏览器是多线程的,所以 JS 引擎需要通过 Event Loop 来实现异步。
浏览器线程包括:
- GUI 渲染线程
- JS 引擎线程
- 事件触发线程(onclick 等)
- 定时触发器线程
- 异步 http 请求线程
GUI 渲染线程和 JS 引擎线程是互斥的,虽然两者属于不同的线程,但是由于 JS 执行结果可能会对页面产生影响,所以浏览器对此做了限制,只有当 JS 引擎空闲时,GUI 线程才能被激活。
MacroTask(宏任务):包括整体代码 script,setTimeout,setInterval,I/O,UI 渲染等。
MicroTask(微任务):Promise,process.nextTick,Object.observe,MutationObserver。
页面重排(Reflow)和重绘(Repaint)
重排:当 DOM 的变化影响了元素的几何信息(尺寸,位置),浏览器需要重新计算元素的几何属性,将其安放在界面中的正确位置,这个过程叫做重排,也叫回流,简单地说就是重新生成布局,重新排列元素。
那些情况会导致重排:
- 页面初始渲染,这是开销最大的一次重排
- 添加/删除可见的 DOM 元素
- 改变元素位置
- 改变元素尺寸,如边距、填充、边框、宽高等
- 改变元素内容,如文本或图片等
- 改变元素字体大小
- 改变浏览器窗口大小,如 resize
- 激活 CSS 伪类,如::hover
- 设置 Style 属性的值,因为通过设置 Style 属性改变节点样式的话,每一次设置都会触发一次 reflow
重排影响范围: 浏览器渲染界面是基于流式布局模型的,所以触发重排时会对周围 DOM 重新排列,影响的范围有两种:全局范围:从根节点 html 开始对整个渲染树进行重新布局。局部范围:对渲染树的某部分或某一个渲染对象进行重新布局。
重排的代价是高昂的,应该尽可能减少重排的次数,减少重排的范围。 优化重排的方法:
减少范围:
- 尽可能在底层级的 DOM 节点上修改样式,避免影响到上层节点
- 不要使用 table 布局,可能很小的一个小改动会造成整个 table 的重新布局
减少次数
- 样式集中改变
- 分离读写操作,避免多次触发重排
- 将 DOM 离线后修改,修改完成后再添加到文档中,使用
documentFragment
在内存中修改 DOM,修改完成后再添加到文档中 - 使用 absolute 或 fixed 定位脱离文档流,避免影响到其他元素
- 优化动画
重绘(Repaint)
当一个元素的外观发生改变,但没有改变布局,重新把元素外观绘制出来的过程叫做重绘。