js执行机制
变量提升
调用栈
1. 在执行之前就进行编译并创建执行上下文
- 当 JavaScript 执行全局代码的时候,会编译全局代码并创建全局执行上下文,而且在整个页面的生存周期内,全局执行上下文只有一份。
- 当调用一个函数的时候,函数体内的代码会被编译,并创建函数执行上下文,一般情况下,函数执行结束之后,创建的函数执行上下文会被销毁。
- 当使用 eval 函数的时候,eval 的代码也会被编译,并创建执行上下文。
2. 调用栈
JavaScript 中有很多函数,经常会出现在一个函数中调用另外一个函数的情况,调用栈就是用来管理函数调用关系的一种数据结构。
调用栈特点:后进先出
JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈
var a = 1;
function sum(b,c){
return b+c
};
function addSum(d,e){
var f = 10;
result = add(d,e);
return a+result+f
};
addSum(3,6)
第一步:创建全局上下文,并将其压入栈底,执行a = 1;
第二步:调用addSum函数,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈中
第三步:当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈
第四步:当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add 函数的返回值,也就是 9
第五步:addSum 执行最后一个相加操作后并返回,addSum 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了
作用域链和闭包
1. 作用域
作用域是指在程序中定义变量的区域,该位置决定了变量的生命周期。通俗地理解,作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。
在 ES6 之前,ES 的作用域只有两种:全局作用域和函数作用域。
-
全局作用域中的对象在代码中的任何地方都能访问,其生命周期伴随着页面的生命周期。
-
函数作用域就是在函数内部定义的变量或者函数,并且定义的变量或者函数只能在函数内部被访问。函数执行结束之后,函数内部定义的变量会被销毁。
ES6支持块级作用域
-
块级作用域特点:在代码块内部定义的变量在代码块外部是访问不到的,并且等该代码块中的代码执行完成之后,代码块中定义的变量会被销毁。
-
**块级作用域形式:**就是使用一对大括号包裹的一段代码,比如函数、判断语句、循环语句,甚至单独的一个{}都可以被看作是一个块级作用域。
//if块 if(1){} //while块 while(1){} //函数块 function foo(){} //for循环块 for(let i = 0; i<100; i++){} //单独一个块 {}
-
ES6中如何使块级作用域生效:使用let和const关键字
for(var i = 0; i<100; i++){ } console.log(i)
for(let i = 0; i<100; i++){ } console.log(i)
引申考题:隔一秒钟打印出来一个自然数,自然数递增
for(let i = 0; i<10; i++){ setTimeout(function(){ console.log(i) },1000*i) }
for(var i = 0; i<10; i++){ setTimeout(function(){ console.log(i) },1000*i) }
let、const关键字解决var关键字变量提升的问题
-
由于变量提升,变量值容易被覆盖
var myname = "王美丽" function showName(){ console.log(myname); if(0){ var myname = "闷倒驴" } console.log(myname); } showName()
-
本该销毁的变量销毁不掉
for(var i = 0; i<100; i++){ } console.log(i)
let、const关键字解决问题的原理
-
let和const关键字创建的变量存储在词法环境中,var关键字创建的变量存储在变量环境中
-
块级内部代码执行结束,立马销毁内部let、const创建的变量
-
let和const创建的变量,初始化不提升,创建提升,所以造成暂时性死区
-
访问变量先在当前执行上下文的词法环境中查找,再到变量环境中查找
function fun(){ var a = 11; let b = 22; { let b = 33; var c = 44; let d = 55; console.log(a); console.log(b); } console.log(b); console.log(c); console.log(d); } fun()
第一步:刚开始执行fun函数
第二步:执行内部代码块
第三步:执行代码块中console.log(a) console.log(b)
第四步:代码块执行结束,相应词法环境中的变量弹出
第五步:执行console.log(b);console.log©;
第六步:执行console.log(d),找不到报错
关于变量提升问题
- var的创建和初始化被提升,赋值不会被提升。
- let的创建被提升,初始化和赋值不会被提升,所以会造成暂时性死区(就是访问不到)。
- function的创建、初始化和赋值均会被提升。
作用域的特点:是代码编译阶段就决定好的,和函数是怎么调用的没有关系。
function bar() {
var myName = "王美丽";
let test1 = 100;
if (1) {
let myName = "丑八怪";
console.log(test);
}
}
function foo() {
var myName = "闷倒驴";
let test = 2;
{
let test = 3;
bar();
}
};
var myName = "大喇叭";
let myAge = 10;
let test =1;
foo();
2. 作用域链
作用域链:当一个函数中使用了某个变量,首先会在自己内部作用域查找,然后再向外部一层一层查找,直到全局作用域,这个链式查找就是作用域链
let num = 1;
function fun1 (){
function fun2(){
function fun3(){
console.log(num);
}
fun3()
}
fun2()
}
fun1();
3.闭包
3.1 什么是闭包
function foo() {
var myName = "王美丽";
let test1 = 1;
const test2 = 2;
var innerBar = {
getName:function(){ console.log(test1) return myName },
setName:function(newName){ myName = newName }
}
return innerBar
}
var bar = foo();
bar.setName("闷倒驴");
bar.getName();
console.log(bar.getName())
3.2 闭包形成的原理
- 作用域链,当前作用域可以访问上级作用域中的变量
- 全局变量只用页面关闭才会销毁
3.3 闭包解决的问题
- 函数作用域中的变量在函数执行结束就会销毁,但是有时候我们并不希望变量销毁
- 在函数外部可以访问函数内部的变量
3.4 闭包带来的问题
-
容易造成内存泄露
-
内存泄漏:占用的内存没有及时释放,内存泄露积累多了就容易导致内存溢出
-
闭包
function fn1() { var a = 4 function fn2() { console.log(++a) } return fn2 } var f = fn1() f()
-
-
3.5 闭包的应用
-
模仿块级作用域
for(var i = 0; i < 5; i++) { (function(j){ setTimeout(() => { console.log(j); }, j * 1000); })(i) }
for (var i = 0; i < lis.length; i++) { (function (j) { lis[j].onclick = function () { alert(j) } })(i) }
-
埋点计数器
function count () { var num = 0; return function(){ num++; return num; } } var num = count();
-
柯里化
function curryingCheck(reg) { return function (txt) { return reg.test(txt) } } var isPhone = curryingCheck(/^(13[0-9]|14[5|7]|15[0|1|2|3|4|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$/) console.log(isPhone('15810606459')) // true var isEmail = curryingCheck(/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/) console.log(isEmail('wyn@nowcoder.com')) // false
this指向
1. this关键字由来
this关键字由来:在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套 this 机制。
var bar = {
myName:"闷倒驴",
printName: function () { console.log(myName) }
}
let myName = '王美丽';
bar.printName(); // '王美丽'
var bar = {
myName:"闷倒驴",
printName: function () { console.log(this.myName) }
}
let myName = '王美丽';
bar.printName(); // '王美丽'
作用域链和 this 是两套不同的系统,它们之间基本没太多联系
2. this在哪里可以使用
全局上下文中的this
console.log(this)来打印出来全局执行上下文中的 this,最终输出的是 window 对象。所以你可以得出这样一个结论:全局执行上下文中的 this 是指向 window 对象的。这也是 this 和作用域链的唯一交点,作用域链的最底端包含了 window 对象,全局执行上下文中的 this 也是指向 window 对象
函数上下文中的this
-
在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
-
通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身
function foo(){
// 'use strict';
console.log(this)
};
foo() // window
3. this指向总结
this指向总结
-
当函数被正常调用时,在严格模式下,this 值是 undefined,非严格模式下 this 指向的是全局对象 window;
-
通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身
-
ES6 中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数
-
new 关键字构建好了一个新对象,并且构造函数中的 this 其实就是新对象本身
- 当执行 new CreateObj() 的时候,JavaScript 引擎做了如下四件事:
- 首先创建了一个空对象 tempObj;
- 接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象;
- 然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象;
- 最后返回 tempObj 对象。
- 当执行 new CreateObj() 的时候,JavaScript 引擎做了如下四件事:
-
嵌套函数中的 this 不会继承外层函数的 this 值。
var myObj = { name : "闷倒驴", showThis: function(){ console.log(this); // myObj var bar = function(){ this.name = "王美丽"; console.log(this) // window } bar(); } }; myObj.showThis(); console.log(myObj.name); console.log(window.name);
-
解决this不继承的方法
- 内部函数使用箭头函数
- 将在外层函数中创建一个变量,用来存储this,内层函数通过作用域链即可访问
var myObj = { name : "闷倒驴", showThis:function(){ console.log(this); // myObj var bar = ()=>{ this.name = "王美丽"; console.log(this) // window } bar(); } }; myObj.showThis(); console.log(myObj.name); console.log(window.name);
var myObj = { name : "闷倒驴", showThis:function(){ console.log(this); // myObj var self = this; var bar = function (){ self.name = "王美丽"; console.log(self) // window } bar(); } }; myObj.showThis(); console.log(myObj.name); console.log(window.name);
-
4. 改变this指向的方法
4.1 call 和 apply 的共同点
都能够改变函数执行时的上下文,将一个对象的方法交给另一个对象来执行,并且是立即执行的
调用 call 和 apply 的对象,必须是一个函数 Function
4.2 call 和 apply 的区别
call 的写法
Function.call(obj,[param1[,param2[,…[,paramN]]]])
需要注意以下几点:
- 调用 call 的对象,必须是个函数 Function。
- call 的第一个参数,是一个对象。 Function 的调用者,将会指向这个对象。如果不传,则默认为全局对象 window。
- 第二个参数开始,可以接收任意个参数。每个参数会映射到相应位置的 Function 的参数上。但是如果将所有的参数作为数组传入,它们会作为一个整体映射到 Function 对应的第一个参数上,之后参数都为空。
function func (a,b,c) {}
func.call(obj, 1,2,3)
// func 接收到的参数实际上是 1,2,3
func.call(obj, [1,2,3])
// func 接收到的参数实际上是 [1,2,3],undefined,undefined
apply 的写法
Function.apply(obj[,argArray])
需要注意的是:
- 它的调用者必须是函数 Function,并且只接收两个参数,第一个参数的规则与 call 一致。
- 第二个参数,必须是数组或者类数组,它们会被转换成类数组,传入 Function 中,并且会被映射到 Function 对应的参数上。这也是 call 和 apply 之间,很重要的一个区别。
func.apply(obj, [1,2,3])
// func 接收到的参数实际上是 1,2,3
func.apply(obj, {
0: 1,
1: 2,
2: 3,
length: 3
})
// func 接收到的参数实际上是 1,2,3
4.3 call 和 apply 的用途
下面会分别列举 call 和 apply 的一些使用场景。声明:例子中没有哪个场景是必须用 call 或者必须用 apply 的,只是个人习惯这么用而已。
call 的使用场景
1、对象的继承。如下面这个例子:
function superClass () {
this.a = 1;
this.print = function () {
console.log(this.a);
}
}
function subClass () {
superClass.call(this); // 执行superClass,并将superClass方法中的this指向subClass
this.print();
}
subClass();
// 1
subClass 通过 call 方法,继承了 superClass 的 print 方法和 a 变量。此外,subClass 还可以扩展自己的其他方法。
2、借用方法。还记得刚才的类数组么?如果它想使用 Array 原型链上的方法,可以这样:
let domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));
这样,domNodes 就可以应用 Array 下的所有方法了。
原理:执行数组的slice方法,把this指向伪数组
// slice2()
Array.prototype.slice2 = function (start, end) {
start = start || 0
end = start || this.length
const arr = []
for (var i = start; i < end; i++) {
arr.push(this[i])
}
return arr
}
apply 的一些妙用
1、Math.max。用它来获取数组中最大的一项。
let max = Math.max.apply(null, array);
同理,要获取数组中最小的一项,可以这样:
let min = Math.min.apply(null, array);
2、实现两个数组合并。在 ES6 的扩展运算符出现之前,我们可以用 Array.prototype.push来实现。
let arr1 = [1, 2, 3];
let arr2 = [4, 5, 6];
Array.prototype.push.apply(arr1, arr2);
console.log(arr1); // [1, 2, 3, 4, 5, 6]
4.4 bind
bind 的使用
在 MDN 上的解释是:bind() 方法创建一个新的函数,在调用时设置 this 关键字为提供的值。并在调用新函数时,将给定参数列表作为原函数的参数序列的前若干项。
它的语法如下:
Function.bind(thisArg[, arg1[, arg2[, ...]]])
bind 方法 与 apply 和 call 比较类似,也能改变函数体内的 this 指向。
不同的是,bind 方法的返回值是函数,并且需要稍后调用,才会执行。而 apply 和 call 则是立即调用。
来看下面这个例子:
function add (c) {
return this.a + this.b + c;
}
var obj = {a:1,b:2}
add.bind(obj, 5); // 这时,并不会返回 8
add.bind(sub, 5)(); // 调用后,返回 8
如果 bind 的第一个参数是 null 或者 undefined,this 就指向全局对象 window。
在vue或者react框架中,使用bind将定义的方法中的this指向当前类