# 变量提升(需彻底理解)
在当前作用域下(在当前的执行上下文下),在所有JS代码执行之前,把所有带var和所有带function关键字的提前的声明(创建一个变量)或者定义(给一个变量赋值)
带var的值***提前声明***,
带function的***提前声明+定义***。
贴段代码帮助我们理解这个概念:
var getName = function () {
console.log(4);
}
function getName() {
console.log(5);
}
getName(); //输出4
为什么这里的getName()输出的是4呢❓下面我们分两个阶段分析😊:
1、代码执行之前: 处理带有var和function关键字的语句,流程如下:
- var的提前声明,则定义了一个叫getName的变量,***但是不赋值,因为这是代码执行之前!***
- function的提前声明+定义,则定义了一个叫getName的变量,但是发现getName变量已经被定义,那么只是将一个方法赋给了它。此时getName=fuc=>5
2、代码执行时: 给getName复制,此时getName=fuc=>4
# 理解完上面这个概念之后,我们再看下面这道题
function Foo() {
getName = function () {
console.log(1);
}
return this;
}
Foo.getName = function () {
console.log(2);
}
Foo.prototype.getName = function () {
console.log(3);
}
var getName = function () {
console.log(4);
}
function getName() {
console.log(5);
}
Foo.getName(); // 2
getName(); // 4
Foo().getName(); // 1
getName(); // 1
new Foo.getName(); // 2
new Foo().getName(); // 3
new new Foo().getName(); // 3
# 解析:
明天再写吧.....头疼😫
和老友叙旧2个多小时,为了兄弟们的终身大事,我操碎了心😂!这部分明天周六补上!
周六早上起床真的太难了!!!
# 咱们继续(拖延症得治!!)(ง •_•)ง:
根据上面的变量提升的概念,我们知道在代码运行之前是***变量提升***。
1、所以第一步(变量提升): 寻找function和var关键字的代码
// 第一个function Foo() { ... },function关键字,声明+定义
Foo = AAAFFFOOO // 这里的AAAFFFOOO假设是Function对象在堆内存中的引用地址
当前内存图:
// 第一个var getName = ... ,var关键字只声明
getName = undefined // 再次再强调一遍,var只声明,不定义,所以此时getName的值是undefined
当前内存图:
// 第二个function getName() { ... },function关键字,声明+定义。但是发现getName变量已经在上一步被声明,所以只需要定义就行,即赋值即可
getName = func=>5 // 这里是伪代码,func=>5表示输出5的那个方法在堆内存中的引用地址,后续将全部按照这种写法
至此,变量提升完毕!我们可以看到的是,目前***全局的getName指向的是func=>5***
当前内存图:
2、接下来第二步(代码执行): 我们一句一句来看
<第1句 />
function Foo() {
getName = function () {
console.log(1);
}
return this;
}
变量提升阶段已完成,跳过,当前内存图如下:
<第2句 />
Foo.getName = function () {
console.log(2);
}
为Foo添加属性getName,该属性也是一个引用类型,当前内存图如下: <第3句 />
Foo.prototype.getName = function () {
console.log(3);
}
为Foo的原型对象(prototype)添加属性getName,该属性也是一个引用类型,当前内存图如下: <第4句 />
var getName = function () {
console.log(4);
}
注意!!!! 这一步是一个赋值操作,因为我们现在处于方法执行阶段,而非变量提升阶段,所以这里一定不能搞混了。当前的内存图为:
<第5句 />
function getName() {
console.log(5);
}
变量提升阶段已完成,跳过,内存图不变,同上一步。
<第6句 />
Foo.getName();
根据方法内存图,很容易可以看到Foo.getName的引用指向的是输出为2的方法,因此该语句输出如下:
2
<第7句 />
getName();
根据方法内存图,很容易可以看到当前全局下的getName变量的引用指向的是输出为4的方法,因此该语句输出如下:
4
<第8句 />
Foo().getName();
根据JS的运算符执行优先级 (opens new window),先计算Foo(),Foo的方法执行执行栈(ECStack)如下图所示:
此时的内存图为:
根据方法的执行栈可以看出来,Foo()执行返回了this也就是windows对象,接下来计算的就是window.getName(),即全局的getName方法,根据当前的内存图可以看到目前全局的getName的引用指向是输出为1的方法,因此该语句输出如下:
1
<第9句 />
getName();
第八句代码执行分析,我们已知,目前全局的getName的引用指向是输出为1的方法,因为该语句输出如下:
1
<第10句 />
new Foo.getName(); // 2
根据JS的运算符执行优先级 (opens new window),当前优先级最高的是成员访问(… . …)运算符,即先执行Foo.getName,而Foo.getName指向的是输出为2的方法。接下来优先级最高的就是new(带参数列表)(new … ( … ))运算符了,因为上面代码可以理解拆分成下面这两步:
var s = Foo.getName;
var r = new s();
new一个方法,对于方法内部,相当于把这个方法当成一个普通方法执行了,因此该语句输出如下:
2
<第11句 />
new Foo().getName();
运算符优先级如下
1. new Foo() // 看成一个整体xxx
2. xxx.getName()
首先new Foo()创建了一个Foo的实例,然后xxx.getName()相当于访问Foo实例的getName(),先从其自身上面找有无getName()方法,发现没有,则去其原型链找,发现原型链上有getName,并且根据我们上面的内存图中可以看出引用指向的是输出为3的方法,因此该语句输出如下:
3
这里有个问题需要注意,上面的第二句代码Foo.getName = function(){ ... }是给Foo这个对象定义的属性方法,并非***Foo实例***的属性方法,这一点一定注意!! 给Foo实例定义方法我们一般是这么写的:
var Foo = function(){
this.getName=function(){ ... }
}
<第12句 />
new new Foo().getName();
运算符优先级如下
1. new Foo() // 看成一个整体xxx
2. xxx.getName // 看成一个整体zzz
3. new zzz()
由第11句可以看出来new Foo().getName既是挂在***Foo实例***的原型对象上的方法,引用指向了输出为3的方法,new一个方法,对于方法内部,相当于把这个方法当成一个普通方法执行了,因此该语句输出如下:
3
# 总结
看似简单的一道面试题里面埋了多少的坑,考察了多少知识点,有堆栈内存,有面向对象,有运算符的优先级,有原型,有闭包等等,所以真的是面试造火箭,工作拧螺丝,但是这些知识掌握不透彻,螺丝能拧好吗?