前言
本篇章是偏了解性的博客,次要讲述在 js 环境中变量、办法执行形式。了解执行程序,可能更好地帮忙你在开发中解决奇奇怪怪的问题。
面试答复
1. 执行上下文:执行上下文能够简略了解成一个对象,这个对象蕴含变量对象、作用域链、this 指向,个别就全局执行上下文和函数执行上下文。
2. 变量晋升:变量晋升就是在赋值操作之前,就应用对应的变量,导致变量变成 undefined。起因在于执行过程中,首先会建设流动对象,而后构建作用域链,再确定 this 指向,最初才是代码执行。创立变量或者函数的步骤都在建设流动对象阶段,而赋值操作是在代码执行阶段,所以才会找不到。
3.this:this 永远指向函数运行时所在的对象,而不是函数被创立时所在的对象。扭转 this 指向通常有三种办法,bind、call、apply,bind 会返回一个新函数,call 在扭转 this 指向后还执行了函数,且可能接管多个参数,而 apply 与 call 的区别在于 apply 接管数组作为传入参数。
4. 手写 apply:首先判断传入的参数是否为值类型,如果是值类型,则间接返回该值类型,如果是援用类型,则给该参数增加 fn 属性用来保留以后 this,这个 this 指向以后的调用函数。下一步判断是否存在其余参数,如果有就将它开展,并将它作为参数传入到下面的 this 函数中,并把执行后果保留到 result 里,而后删除 fn 属性,并返回 result。至于 call 与 apply 的区别在于传入的参数不一样,bind 与 apply 的区别在于 bind 返回一个新函数并不执行。
5. 事件循环(Event Loop):事件循环是浏览器的一种解决 JS 单线程运行时不阻塞的机制,具体流程是这样的:1、首先所有同步工作都在主线程上执行,造成一个执行栈。2、如果遇到了异步工作,就丢到主线程外的工作队列,等异步工作有后果后,就会转移到调用栈中。3、再而后执行栈中所有同步工作执行结束,就会读取调用栈,如果有工作就丢到执行栈,开始执行这个工作中同步工作。4、最初主线程一直反复下面的几个步骤,这就是事件循环的一个机制。从执行程序上来看,就是一个主线程 > 微工作 > 宏工作,有后果 > 宏工作,无后果的程序。
知识点
javascript 函数执行过程次要由创立执行环境、进入函数调用栈、执行、销毁这四个阶段形成,上面咱们来一一了解每一个阶段所做的事件。
1. 创立执行环境
执行环境,也就是执行上下文,分为全局环境、函数环境、Eval 函数执行环境。
全局环境指的是 JS 默认的代码执行环境,是最外围的一个执行环境,在 web 浏览器中,全局执行环境被认为是 window 对象。一旦代码被载入,引擎最先进入的是这个环境,全局环境不会被主动回收,只有在敞开浏览器窗口的时候才会被销毁,所以在定义全局变量肯定要分外小心。
函数环境是一个绝对于全局环境的概念,因为在执行代码时,线程就是在全局环境和函数环境之间来回穿梭的,能够简略了解为函数环境即任何一个函数被调用都会创立一个新的执行环境,执行完结后返回全局环境,而创立的函数环境期待垃圾回收。
Eval 函数执行环境不常常用,尽量避免,这里不做探讨。
2. 函数调用栈
在创立执行环境后,函数 / 代码下一个阶段会被放入一个栈中,这个栈被称为函数调用栈。js 依据函数的调用 (执行) 来决定执行程序。函数调用栈的栈底永远都是全局环境,而栈顶就是以后正在执行函数的环境。当栈顶的执行环境执行完之后,就会出栈,并把执行权交给之前的执行环境。举例:
function A(){console.log("this is A");
function B(){console.log("this is B");
}
B();}
A();
1. 首先 A() ;A 函数执行了,A 执行环境入栈。
2.A 函数执行时,又调用了 B(),B 又执行了,B 入栈。
3.B 中没有可执行的函数了,B 执行完出栈。
4. 继续执行 A,A 中没有可执行的函数了,A 执行完 出栈。
上述例子只是为了说明函数调用栈的作用,具体的执行过程,包含执行上下文、作用域链、this 指向等操作是在执行过程产生的。
3. 执行过程
当函数被调用时,会创立一个新的函数执行环境,该创立过程次要由两个阶段组成:建设阶段、代码执行阶段
A. 建设阶段(产生在调用 / 执行一个函数时,然而在执行函数外部的具体代码之前)
1. 建设流动对象
2. 构建作用域链
3. 确定 this 的指向
B. 代码执行阶段
1. 执行函数外部的具体代码
接下来,咱们一一了解其中的步骤:
A.1. 建设流动对象
这里咱们首先了解两个概念:变量对象(Variable object,VO)、流动对象(Activation object)
变量对象(Variable object,VO) 是一个与执行上下文相干的非凡对象,在函数上下文中,VO 是不能间接拜访的。变量对象用来存储上下文的函数申明、函数形参、变量申明。优先级:函数申明 > 函数的形参 > 变量。
函数申明:每找到一个函数申明,就在流动对象上面用函数名建设一个属性,属性值就是指向该函数在内存中的地址的一个援用, 如果上述函数名曾经存在于流动对象下,那么则会被新的函数援用所笼罩。
函数形参:建设 arguments 对象,查看以后上下文中的参数,建设该对象下的属性以及属性值。没有实参的话,属性值为 undefined。
变量申明:每找到一个变量申明,就在流动对象上面用变量名建设一个属性,该属性值为 undefined。如果变量名称跟曾经申明的 形式参数 或函数 雷同,则变量申明不会烦扰曾经存在的这类属性。
流动对象(Activation object,AO):因为变量对象不能拜访,在函数执行阶段,由变量对象转化而来的可拜访对象。
举例:
var a = 10;
function b () {console.log('全局的 b 函数')
};
function bar(a, b) {console.log('1', a, b)
var a = 1
function b() {console.log('bar 下的 b 函数')
}
console.log('2', a, b)
}
bar(2, 3)
console.log('3', a, b)
解析:我这边的了解跟参考资料有所不同,望斧正。
// 创立阶段:// 第一步,遇到了全局代码,进入全局上下文,此时的执行上下文栈是这样
ECStack = [
globalContext: {
VO: {
// 依据优先级程序会优先解决全局下的 b 函数申明, 值为该函数所在内存地址的援用
b: <reference to function>,
// 紧接着,按程序再解决 bar 函数申明
bar: <refernce to function>,
// 依据 1.3, 再解决变量,并赋值为 undefined
a: undefined
}
}
];
// 第二步,调用 bar 函数,就又新建了一个函数上下文,此时的执行上下文栈是这样
ECStack = [
globalContext: {
VO: {b: <reference to function b() {}>,
bar: <refernce to function bar() {}>,
a: undefined
}
},
<bar>functionContext: {
VO: {
// 依据优先级,先申明函数 b, 并且赋值为 b 函数所在内存地址的援用;// 紧接着,形参 b 反复,优先级低于函数 b,因而,疏忽新的申明,而 a 为 2;// 最初是变量申明 a,因为 a = 2 曾经存在,因而新的变量申明,a=undefined 会被疏忽,最初变量状况如下:b: <refernce to function b() {}>,a: 2
}
}
]
console.log('1', a, b): '1' , 2 , function b(){console.log('bar 下的 b 函数')}
console.log('2', a, b): '2' , 1 , function b(){console.log('bar 下的 b 函数')} , 这里 a 为 1,是因为执行了代码,把 1 赋值给了 a
console.log('3', a, b): '3' , 10 , function b(){console.log('全局的 b 函数')} ,a 为 10,function b(){console.log('全局的 b 函数')}是因为 bar 函数曾经执行完了,就从调用执行栈中弹出。
块级作用域,简略来说就是函数外部和 {} 之间的局部。变量晋升也是在建设阶段产生的问题,即在赋值操作之前(赋值操作在代码执行阶段),就应用对应的变量,从而使变量为 undefined,举例:
console.log(a)
var a = 100
function b(){console.log(a)
var a = 200
console.log(a)
}
b()
console.log(a)
// undefined undefined 200 100
var foo = function (){console.log(1)
}
function foo(){console.log(2)
}
foo() //1
转化一下 -->
function foo(){console.log(2)}
var foo
// 因为下述代码曾经属于代码执行阶段,所以上述规定不能利用于此处
//function foo 与 var foo 属于同一个内存地址,所以可能产生笼罩,下述的赋值,即起到笼罩的作用,所以输入的为 1
foo = function (){console.log(1)}
foo()
参考资料:https://article.itxueyuan.com/O0mA6
A.2. 构建作用域链
作用域链的最前端,始终都是以后执行的代码所在函数的流动对象。下一个流动对象(AO)为蕴含本函数的内部函数的 AO,以此类推。最末端,为全局环境的变量对象。
留神:
1. 尽管作用域链是在函数调用时构建的,然而它跟调用程序 (进入调用栈的程序) 无关,因为它只跟蕴含关系 (函数、蕴含函数的嵌套关系) 无关。
2. 作用域链是创立函数的时候就创立了,此时的链只有全局变量对象,保留在函数的 [[Scope]] 属性中,而后函数执行时的,只是通过复制该属性中的对象来构建作用域链。
举例:
function a(){
var va =1
function b(){
var vb = 2
console.log(va)
console.log(vb)
}
b()}
a()
b 函数被 a 函数蕴含,a 函数被 window 全局环境蕴含。
参考资料:https://blog.csdn.net/weixin_33919950/article/details/89625339
A.3. 确定 this 的指向
this 的指向在函数定义的时候是确定不了的,只有函数被调用的时候能力确定,并且 this 的最终指向的是那个调用它的对象。
状况 1:匿名函数
匿名函数 this 的默认指向为 windows
var age =1
var obj = {
age:2,
getAge:function(){console.log('getAge',this.age)
return function(){console.log('unkonw',this.age)
}
}
}
obj.getAge()()
//getAge 2
//unkonw 1
状况 2:函数调用
1. 如果一个函数中有 this,然而它没有被上一级的对象所调用,那么 this 指向的就是 window。
var va = 'abc'
function a(){
var va = 123
console.log(this) //window
console.log(this.va) //'abc'
}
a()
2. 如果一个函数中有 this,这个函数有被上一级的对象所调用,那么 this 指向的就是上一级的对象。
var obj={
a:'abc',
b:function(){console.log(this.a) //'abc'
}
}
obj.b()
3. 如果一个函数中有 this,这个函数中蕴含多个对象,只管这个函数是被最外层的对象所调用,this 指向的也只是它上一级的对象,也就是说 this 指向的是间接调用它的对象。
var obj={
a:123,
b:{
a:'abc',
b:function(){console.log(this.a) //'abc'
}
}
}
obj.b.b()
var a =1;
var obj = {
a:2,
b:{
a:3,
getValue:function(){console.log(this.a)
}
}
}
console.log(obj.b.getValue());//obj.b.getValue()这里 this 指向 obj.b, 输入 3
var test = obj.b.getValue;// 此时仅做赋值,并没有调用函数,因为是.getValue 而不是 getValue()
console.log(test()); // 此时调用函数合乎第一种状况,所以 this 指向 window
状况 3:构造函数中的 this 指向
首先 new 关键字会创立一个空的对象,而后会主动调用一个函数 apply 办法,将 this 指向这个空对象,这样的话函数外部的 this 就会被这个空的对象代替,能够参考后续对于 new 操作符的知识点。
function a(){console.log(this) //a {}}
var va = new a()
当构造函数的 this 碰到 return 时:如果返回值是一个对象,那么 this 指向的就是那个返回的对象,如果返回值不是一个对象那么 this 还是指向函数的实例。
function a(){
this.a = 1
return {a:'abc'}
}
var va = new a()
console.log(va.a) //'abc'
状况 4:箭头函数的 this 指向
箭头函数的 this 指向,是指向箭头函数 被创立时内部作用域(要么是 window,要么是最近一层的部分函数)的 this 指向的对象,而不是调用时指定 this 指向。举例:
var name = 'window';
var A = {
name: 'A',
sayHello: () => {console.log(this.name)
}
}
A.sayHello();
// 此时箭头函数内部作用域为 window,所以指向的是 window
var name = 'window';
var A = {
name: 'A',
sayHello: function(){var s = () => console.log(this.name)
return s// 返回箭头函数 s
}
}
var sayHello = A.sayHello(); // 这里拿到的 s,因为 return 了
sayHello();
//function()的 this 指向为对象 A(因为 A.sayHello,function 被调用了,参考状况 1),而箭头函数内部的 this 指向即为 function()的 this 指向这个的 this 指向 对象 A。这跟上述定义并不抵触,箭头函数的 this 指向依然指向 function 的 this,只不过 function 的 this 指向是在调用的时候才确定。
状况 5:call、bind、apply 的 this 指向
因为 js 中 this 的指向受函数运行环境、调用的影响,指向常常扭转,使得开发变得艰难和含糊,所以在写一些简单函数的时候常常会用到 this 指向绑定,以避免出现不必要的问题,call、apply、bind 根本都能实现这一性能。
1.bind:bind 用于将函数体内的 this 绑定到某个对象,而后返回一个新函数
var counter = {
count: 0,
inc: function () {this.count++;}
}
var obj = {count: 100};
var func = counter.inc.bind(obj);
func();
obj.count // 101
2.call:call 办法能够指定 this 的指向,而后再指定的作用域中,执行函数。call 能够承受多个参数,第一个参数是 this 指向的对象,之后的是函数回调所需的入参
var newObj = {a:1,b:2}
var a = 11
var b = 22
var obj = {
a:111,
b:222,
fn:function test(val){return this.a + this.b + val}
}
var func = obj.fn
func.call(newObj,4) //7
3.apply:apply 和 call 作用相似,也是扭转 this 指向,而后调用该函数,惟一区别是 apply 接管数组作为函数执行时的参数
var newObj = {a:1,b:2}
var a = 11
var b = 22
var obj = {
a:111,
b:222,
fn:function test(a,b){return this.a + this.b + a + b}
}
var func = obj.fn
func.apply(newObj,[3,4]) // 10
PS:call、bind、apply 能实现以下根底性能,
- 间接调用函数,扭转 this,劫持其余对象的办法
- 两个函数实现继承
-
为类数组 (arguments 和 nodeList) 增加数组办法,如 push、pop
(function(){Array.prototype.push.call(arguments,'王五'); console.log(arguments);//['张三','李四','王五'] })('张三','李四')
- 合并数组、求数组内最大值
4. 面试题:手动实现 bind、call、apply
实现 call(obj,arg,arg….)
1. 扭转 this 的指向。
2. 传入参数。
3. 返回函数执行后果
// 函数原型上增加 myCall 办法来模仿 call,这样就能够间接在函数后进行调用
Function.prototype.myCall = function (context) {let currentObj = typeof context ==='object' ? context : {} // context 就是传入的第一个参数
currentObj.fn = this // 设置一个长期属性,把 this 的值给这个长期属性,这里的 this 指向的是调用 myCall 办法的上一级对象(函数),将 parent 函数存起来,parent 调用的 myCall,此时 this 指向的就是该办法。let arg = [...arguments].slice(1) // 将参数中除了第一个参数之后的全副存起来
currentObj.fn(...arg) // 将参数传入,此时应用第一个参数调用父级函数,使父级函数的 this 指向指为第一个参数(参考 A.1. 状况 1)delete currentObj.fn // 将函数删除,防止长期属性的烦扰。}
var a = {value: 1}
function test(a,b) {console.log(this.value + a + b)
}
test.myCall(a, 2, 3) //6
实现 apply(obj, [params1, params2, ….])
apply 与 call 的区别在于传参,所以把 let arg = […arguments].slice(1) 替换一下,如下:
Function.prototype.myApply = function (context) {let currentObj = typeof context ==='object' ? context : {} // 这里就是传入的第一个参数
currentObj.fn = this // 将 parent 函数存起来,parent 调用的 myCall,此时 this 指向的就是该办法
if(arguments[1]) {currentObj.fn(...arguments[1])
} else {currentObj.fn()
}
delete currentObj.fn
}
var a = {value: 1}
function test(a,b) {console.log(this.value + a + b)
}
test.myApply(a, [2,3])
实现 bind(obj, …arg)
bind 函数与 call、apply 的区别有两点:一是返回一个新函数;二是柯里化,柯里化举例如下:
function add(x) {return function(y) {return x + y;};
};
add(1)(2); // 3
实现 bind 代码:
// 实现 bind()办法, 调用 bind()必须是一个函数, 因为要返回一个新函数;// 能够通过 new 批改 this,new 的优先级最高 bind()能够将参数分两次传递进来
Function.prototype.myBind = function (context) {if(typeof this !== "function") { // 如果不是函数则间接抛出
throw new TypeError('Error')
}
let self = this // 保留 this, 即为 parent
let arg = [...arguments].slice(1) // 将参数中除了第一个之后的全副存起来
// bind()返回的是一个函数,所以能够应用 new,并且会批改 this 的指向
return function F() {if(this instanceof F) { // 如果 new 执行此时即为 true
return new self(...arg, ...arguments) // 返回 new parent(第一次传递的参数,第二次传递的参数)-->arguments 是执行返回的函数时的参数
}else{return self.apply(context, [...arg, ...arguments]) // 如果没有执行 new,那么间接执行 parent, 通过 apply 会将 this 执行最后传进来的对象 a
}
}
}
B.1. 执行函数外部的具体代码
执行代码阶段最次要有两个事件:变量赋值、JS 事件机制。其中变量赋值比拟好了解,这里不做过多解释,接下来着重了解 JS 事件机制。
JavaScript 是单线程指的是同一时间只能干一件事件,只有后面的事件执行完,能力执行前面的事件。导致遇到耗时的工作时前面的代码无奈执行,因而有了同步、异步工作。
同步工作:for 循环、new Promise 等除了异步工作外的其余工作
异步工作:微工作、宏工作,其中微工作有 Promise.then、process.nextTick、queueMicrotask,宏工作有 setTimeout、setInterval、IO、UI 渲染等
async/await 只是一个语法糖,只是帮忙咱们返回一个 Promise 而已,如果办法被调用的话,async 相当于 new Promise,await 前面的代码相当于.then 里的代码,且.then 代码必须得在 promise 中 resolve 后才会执行,否则.then 代码不会执行,await 在 async 代码执行结束后,进入微工作队列。
理解工作的概念后,当初整顿一下工作执行的流程:
1、所有同步工作都在主线程上执行,造成一个执行栈(execution context stack)。
2、主线程之外,还存在一个工作队列(task queue)。只有是异步工作,就丢到这个工作队列,有了运行后果,就在工作队列之中搁置一个事件。
3、当工作队列中的异步工作有了后果之后,就会将工作挪动到调用栈中。
4、一旦执行栈中的所有同步工作执行结束,零碎就会读取调用栈,开始执行该异步工作中同步工作。
5、主线程一直反复下面的第三步,称为事件循环(Event Loop)。
总结执行程序:主线程(同步工作,new Promise)> Promise.then(微工作)> setTimeout(宏工作,有后果)> setTimeout(宏工作,无后果),同类型工作依照先进先出的程序执行。
面试题:
async function async1() {console.log(2);
await async2();
console.log(7);
}
async function async2() {new Promise(function (resolve) {console.log(3);
resolve();}).then(function () {console.log(6);
});
}
console.log(1);
setTimeout(function() {console.log(8);
}, 0)
async1();
new Promise(function(resolve, reject){console.log(4)
setTimeout(function(){console.log(9)
resolve(13)
console.log(10)
setTimeout(function(){console.log(12)
})
}, 0)
}).then(res =>{ // flag
console.log(11)
setTimeout(() =>{console.log(13)
}, 0)
})
console.log(5);
//1-13 按程序执行,参考上述执行程序及规定。
4. 销毁执行环境和流动对象
某个执行环境所有代码执行完之后,该环境被销毁,保留在其中的所有变量和函数定义也随之销毁,这边闭包有所不同,将会在下篇博客中,进一步了解。全局执行环境只会在关了浏览器或者程序的时候才被销毁。
最初
走过路过,不要错过,点赞、珍藏、评论三连~