JS面试一览

92 阅读6分钟

执行上下文

当 JavaScript 代码执行一段可执行代码(全局代码、函数代码、eval代码)时,会创建对应的执行上下文

  • 对于每个执行上下文,都有三个重要属性:变量对象(Variable object,VO)、 作用域链、 this
  • 执行上下文栈(先进后出,全局执行上下文最先进入,最后弹出)

js引擎创建了执行上下文栈,来管理执行上下文,初始化的时候首先就会向栈内压入一个全局执行上下文,当执行一个函数时,会创建它的执行上下文压入栈内,在函数执行完后它的执行上下文才会从栈中弹出,整个应用程序结束的时候,全局执行上下文才会被弹出,栈被清空

变量对象:

存储了在执行上下文中定义的变量和函数声明。

  • 预编译(函数调用时且在具体的函数代码运行之前)
- 函数预编译 - (函数被调用时,变量对象VO被激活为活动对象AO)

   1. 创建VO对象
   
   2. 找形参和变量声明,将变量和形参作为VO的属性名值为undefined
   
   3. 将实参值和形参统一  
   
   4. 在函数内部找函数声明, 值赋予函数体
   
- 全局预编译 - 关于GO

   1. 创建GO == window对象
   
   2. 找变量声明 
   
   3. 找函数声明赋值函数体

作用域链(Scope chain)

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

  • 作用域:规定了当前执行代码对变量的访问权限,可以隔离变量,不同作用域下同名变量不会冲突。

  • 静态作用域和动态作用域

    JavaScript 采用的是词法(静态)作用域,函数的作用域在函数定义的时候就决定了,而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。

    var value = 1;
    function foo() {
        console.log(value);
    }
    function bar() {
        var value = 2;
        foo();
    }
    
    bar();  // 1
    

总结一下函数执行上下文中作用域链和变量对象的创建过程:

var scope = "global scope";
function checkscope() {
    var scope2 = 'local scope';
    return scope2;
}
checkscope();


执行过程如下:

1.当函数创建时,会有一个名为 [[scope]] 的内部属性保存所有父变量对象到其中。
当函数执行时,会创建一个执行环境,然后通过复制函数的 [[scope]]  属性中的对象
构建起执行环境的作用域链,然后,变量对象 VO 被激活生成 AO 并添加到作用域链的前端,
完整作用域链创建完成:
checkscope.[[scope]] = [
    globalContext.VO
];

2. 开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链
checkscopeContext = {
  Scope: checkscope.[[scope]],
}



3. 开始预编译 创建AO
checkscopeContext = {
	AO: { 
		arguments: { length: 0 },
		scope2: undefined
	},
	Scope: checkscope.[[scope]],
}


4. 预编译完成后将AO压入 checkscope 作用域链顶端
checkscopeContext = {
    AO: { 
	    arguments: { length: 0 },
	    scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}


5. 开始执行函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈, 
随着函数的执行,修改 AO 的属性值
ECStack = [
    checkscopeContext,
    globalContext
];
checkscopeContext = {
   AO: {
       arguments: { length: 0 },
       scope2: 'local scope'
   },
   Scope: [AO, [[Scope]]]
}


7. 函数执行完毕,函数上下文从执行上下文栈中弹出
ECStack = [
    globalContext
];

this 可以简单的理解为最后调用函数的对象, ECMAScript规范解读this

  • 手写
// new
function myNew(fn, args) {
    const obj = Object.create(fn.prototype)
    const result = fn.call(obj, args)
// 如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),那么`new`表达式中的函数调用会自动返回这个新的对象。
    return ['object', 'function'].includes(typeof result) ? result : obj
}

// call
Function.prototype.myApply = function (target, args) {
    target = target ? Object(target) : window
    target.fn = this
    return target.fn(args)
}

// bind
Function.prototype.myBind = function (target, args) {
    // 保存原函数的原型,避免共用一个原型的话互相影响(箭头函数没原型)
    const func = function () {}
    func.ptototype = this.prototype
    const self = this
    function bindFunc (args1) {
        // this instanceof bindFunc , 判断是否使用 new 来调用 bindFunc,如果是this的指向就是其实例
        return self.myApply(this instanceof bindFunc ? this : target, [...args, ...args1])
    }
    bindFunc.prototype = myNew(func)
    return bindFunc
}

原型

闭包

理论角度:闭包是指那些能够访问自由变量的函数

自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。

实践角度:以下函数才算是闭包:

  1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  2. 在代码中引用了自由变量

优点 自由变量不会被垃圾回收机制回收

缺点 内存泄漏

解决内存泄漏
function a (){
   let x = 1
    return function b () { x++  }
}

var c = a()
c = null

作用 4. 实现公有变量: 函数累加器 5. 可以做缓存(存储结构) 6. 可以实现封装,属性私有化 7. 模块化开发,防止污染全局变量

EventLoop

js是单线程的脚本语言

js单线程任务被分为同步任务异步任务,同步任务会在执行栈中按照顺序等待主线程依次执行,异步任务会在有了结果后, 将注册的回调函数放到任务队列中等待主线程空闲的时候(执行栈被清空),被读取到栈内等待执行。

  • 上面说的任务一般都指宏任务, 在当前宏任务执行过程中也会有一些微任务,这些微任务的回调会被注册到微任务队列里,js会在把将当前微任务队列里的微任务执行完(更新渲染页面),再执行下一个宏任务,我将其理解为eventloop。

在这里插入图片描述 在这里插入图片描述

Promise

手写

const PENDING = 'PENDING'
const FULFILLED = 'FULFILLED'
const REJECTED = 'REJECTED'
const resolvePromise = (promise, x, resolve, reject) => {
	if (x === promise) return reject(new Error('循环引用,x不能与promise为同一对象'))
	let only = false
	if (x && ['object, function'].includes(typeof x) && x.then instanceof Function) {
		try {
			const then = x.then
			then.call(x, y => {
				if (only) return
				only = true
				resolvePromise(promise, y, resolve, reject)
			}, r => {
				if (only) return
				only = true
				reject(e)
			})
		} catch (e) {
			if (only) return
			only = true
			reject(e)
		}
	} else {
		resolve(x)	
	}
}

class Promise {
	constructor(executor) {
		this.status = PENDING
		this.value = undefined
		this.reason = undefined
		this.resolveCallback = []
		this.rejectCallback = []
		
		const resolve = val => {
			if (this.status === PENDING) {
				this.status = FULFILLED
				this.value = val
				this.resolveCallback.forEach(fn => fn())
			}
		}
		
		const reject = val => {
			if (this.status === PENDING) {
				this.status = REJECTED 
				this.reason = val
				this.rejectCallback.forEach(fn => fn())
			}
		}
		
		try {
			executor(resolve, reject)
		} catch (e) {
			reject(e)
		}
	}

	then(onFulfilled, onRejected) {
		onFulfilled = onFulfilled instanceof Function ? onFulfilled : v => v
		onRejected = onRejected instanceof Function ? onRejected : err => { throw err }
		const promise = new Promise((resolve, reject) => {
			const next = (isResolve = true) => setTimeout(() => {
				try {
					resolvePromise(promise, 
					isResolve ? onFulfilled(this.value) : onRejected(this.reason),
					resolve,
					reject)
				} catch(e) {
					reject(e)
				}
			},0)
			if (this.status === FULFILLED) next()
			if (this.status === REJECTED) next(false)
			if (this.status === PENDING) {
				this.resolveCallback.push(next)
				this.rejectCallback.push(() => next(false))
			}
		})
		return promise
	}
}