js变量查找机制及其补丁

447 阅读5分钟

js中围绕变量、对象属性查找存在着作用域链和原型链两套实现机制,但是这两套机制都存在不足,所以分别引入了闭包和this的概念。

一、执行上下文

js执行代码的过程中,为了区分是在那个环境下执行以及减少内存占用,会产生一个执行上下文。这个执行上下文中包括变量对象、this、以及指向上一个执行上下文的指针。

执行到一个函数时,在编译阶段生成执行上下文,然后推入执行栈中,开始执行、 执行结束的时候推出栈,销毁这个执行上下文。执行上下文的生存期就是指执行上下文从创建到销毁的过程。

img

二、作用域

2.1 为什么需要作用域

为了增强程序的局部逻辑性,减少命名冲突,需要一套规则来管理变量,这套规则就是作用域,它规定了在js中访问变量时首先读取当前执行上下文中变量对象,如果找不到则向上一级作用域中读取。

2.2 作用域的分类

全局作用域

函数作用域

const let的块级作用域

2.3 如何改变作用域

with

eval

2.4 作用域是如何实现的

js中函数执行时会生成执行上下文,执行上下文中有变量对象和执行上一级执行上下文的引用指针等信息,从而形成了一条链式结构,我们称这条链式结构为作用域链。js中读取变量的过程就是对这条链表遍历的过程。

三、闭包

3.1 为什么需要闭包

闭包是为了解决作用域查找规则和执行上下文生存期的矛盾而引入的。

根据生存期,当外部函数执行完毕,它的执行上下文就销毁了;根据作用域查找规则,内部函数总是可以读取外部函数的执行上下文。

为了解决这个矛盾引入了闭包这个解决方案。闭包机制会把内部函数引用的变量存储在堆内存中。

3.2 什么是闭包

💡 函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可访问,就形成了闭包。

3.3 闭包的常见表现

函数作为参数被传递

内部函数被外部引用

3.4 闭包的应用场景

闭包的应用场景主要是两个方面,形成局部变量以及延长变量的生存期

立即执行函数形成私有作用域,来解决class中无私有变量的问题

img

偏函数与柯里化

img

四、原型链

当我们提起原型链,首先想到的角度就是用起来实现继承,但是继承从本质上讲解决的也是对象读取属性值时的查找问题。

当我们声明一个对象时,使用new Object来声明,此时声明出来的这个object内部自带一个proto指向Object这个类的原型,使得其可以通过这条链来读取Object的原型方法。

img

由于本文主要讲述的是js变量查找规则以及为了解决其不足而引入的闭包及this概念,此处暂时跳过原型继承的实现,但是希望读者能够意识到作用域链和原型链都是为了解决变量查找问题而引入的,而引入闭包与this是为了解决这两者存在的不足。

五、this

5.1 为什么需要this

在js中,对象内部的方法无法直接使用对象内部的属性,基于这个需求,JavaScript 引入了this 机制。

5.2 this是什么?

每个函数都有一个隐式的 this 形参,它的指向在函数被调用时确定。

5.3 如果确定this执向

由于this是在函数被调用时确定的,因此根据函数对象的调用方式分为5大类,共7种情况

img

5.4 箭头函数

箭头函数是为了解决内层 函数中的this指向winddow而不是指向其所属的函数中的this问题

因此箭头函数指向其定义位置的函数中的this,且这个this不可以改变

5.5 一些值得注意的问题

[].forEach(fn,context) // forEach第二个参数接收this

(x,obj.fn) 此时fn的this指向window

六、 原型链与this相关api实现原理

6. 1 call、apply、bind实现原理

Function.prototype.myCall  = function(context,...args) {
	const obj = context
	obj.temp = this
	const result = obj.temp(...args)
	delete obj.temp
	return result
}

Function.prototype.Mybind = function(context, ...args) {
	return (...params)=> {
		return this.call(context, ...args, ...params)
	}
}

6.2 instanceof实现原理

function myInstance(source, target) {
	target = target.prototype
	let cur = source
	while(cur) {
		cur = cur.__proto__
		if(cur===target) {
			return true
		}
	}
	return false
}

6.3 new实现原理

// new方法的本质是在创建一个对象,比其工厂函数模式,其解决了对象标示问题和手动创建this,返回this
// 对象的隐式原型指向其构造函数
function myNew(Fn) {
	const obj = Object.create(Fn.prototype)
	const result = Fn.call(obj)
	return typeof result === 'object'?result: obj
}

6.4 es5、es6实现继承

class Parent {
	constructor(food) {
		this.food = food
	}
	eat() {
		console.log(this.name , this.age, this.food)
	}
}
class Child extend Parent {
	constructor(name,age,food) {
		super(food)
		this.name = name
		this.age = age
	}
}

function Parent(food) {
	this.food = food
}

Parent.prototype.eat = function() {
	console.log(`我的名字是${this.name} 今年${age}岁了, 我喜欢吃${food}`)
}

function Child(name, age) {
	Parent.call(this)
	this.name = name
	this.age = age
}

Child.prototype =  Object.create(Parent.prototype)
Child.prototype.constructor = Child

后记:

作用域链与原型链都是js中查找变量、读取属性的具体实现手段,通过作用域链,实现了在js中访问变量时首先读取当前执行上下文中变量对象,如果找不到则向上一级作用域中读取。通过原型链,实现了读取对象属性时首先从当前私有属性中查找,如果找不到则向上一级原型中查找。

虽然这两套机制使得js在短短10余天中诞生,但是都存在这不足,为了解决作用域查找规则与执行上下文生存期的矛盾,不得不引入闭包;同时为了解决对象的方法无法直接访问对象内的属性,不得不引入了this。这两个概念的引入都是一种补丁,由于没有从根本上解决问题,因此存在着各种复杂的表现形式,使得每一个学习js语言的读者在这里不得不记忆大量具体的细则