声明提前
什么是执行上下文?
简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。
执行上下文的类型
JavaScript 中有三种执行上下文类型。
- 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置
this
的值等于这个全局对象。一个程序中只会有一个全局执行上下文。 - 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
- Eval 函数执行上下文 — 执行在
eval
函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用eval
,所以在这里我不会讨论它。
执行栈
执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
当 JavaScript 引擎第一次遇到你的脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。
引擎会执行那些执行上下文位于栈顶的函数。当该函数执行结束时,执行上下文从栈中弹出,控制流程到达当前栈中的下一个上下文。
让我们通过下面的代码示例来理解:
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
上述代码的执行上下文栈。
当上述代码在浏览器加载时,JavaScript 引擎创建了一个全局执行上下文并把它压入当前执行栈。当遇到 first()
函数调用时,JavaScript 引擎为该函数创建一个新的执行上下文并把它压入当前执行栈的顶部。
当从 first()
函数内部调用 second()
函数时,JavaScript 引擎为 second()
函数创建了一个新的执行上下文并把它压入当前执行栈的顶部。当 second()
函数执行完毕,它的执行上下文会从当前栈弹出,并且控制流程到达下一个执行上下文,即 first()
函数的执行上下文。
当 first()
执行完毕,它的执行上下文从栈弹出,控制流程到达全局执行上下文。一旦所有代码执行完毕,JavaScript 引擎从当前栈中移除全局执行上下文。
怎么创建执行上下文?
到现在,我们已经看过 JavaScript 怎样管理执行上下文了,现在让我们了解 JavaScript 引擎是怎样创建执行上下文的。
创建执行上下文有两个阶段:1) 创建阶段 和 2) 执行阶段。
在 JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事:
- this 值的决定,即我们所熟知的 This 绑定。
- 创建词法环境组件。简单来说词法环境是一种持有标识符—变量映射的结构。
- 创建变量环境组件。简单来说在执行上下文中创建的绑定关系。
- 执行程序
function Foo() { getName = function(){return 1;} return this; } Foo.getName=function(){return 2;} Foo.prototype.getName=function(){return 3;} var getName=function(){return 4;} function getName(){return 5;} //以下内容会输出什么? console.log(Foo.getName()) console.log(getName()); console.log(Foo().getName()); console.log(getName()); console.log(new Foo.getName()); console.log(new Foo().getName()); console.log(new new Foo().getName());
结果:
console.log(Foo.getName()) //2 console.log(getName()); //4 console.log(Foo().getName()); //1 console.log(getName()); //1 console.log(new Foo.getName()); //Foo.getName {} console.log(new Foo().getName()); //3 console.log(new new Foo().getName()); //Foo.getName {}
1.Foo.getName()
Foo
构造函数的静态方法,很明显是引用在它本身的方法,输出2
。
2.getName()
这里主要是这两句:
var getName=function(){return 4;}
function getName(){return 5;}
经过声明提升之后会变成这样:
var getName;
function getName(){return 5;}
getName = function(){return 4;}
所以这里是输出4
。
原因:变量和函数同名,函数声明先赋值的,它是在执行上下文的执行阶段一开始的时候就已经进行了赋值操作而,变量声明 (var getName=function(){return 4;} 可看作变量声明
) 是要执行到赋值语句的时候才进行的赋值。应为函数变量同名,最后函数被变量覆盖
3.Foo().getName()
这里有点麻烦,如果习惯用严格模式的话,就能看出来,这里在严格模式下其实是非法的。因为严格模式禁止this关键字指向全局对象。在非严格模式的情况下,先是运行了Foo()
这个构造函数,由于没有用new
,所以其中的this
指向了windows
,在构造函数内部定义的getName
重新覆盖了原来的全局变量,返回的this
实际上就是window
,所以这里输出的是1
。
4.getName()
经过了第三点,全局变量的getName
已经被改写成Foo()
内部的那个,这里当然仍然输出1
。
//不懂???
5.new Foo.getName()
这里的关键是new
干了啥。规范里写的很长,简述以下就是,首先会创建一个新的空对象,然后把这个新对象的[[Prototype]]
指向后面那个对象的[[Prototype]]
,然后将this
指针指向这个新的空对象,最后会调用后面那个对象下属的[[Construct]]
这个内部方法,对它使用[[call]]
,把this
和括号里面的参数都传进去。
在这里的话,new
后面是Foo.getName
,它是一个函数,那么很明显将调用Function
构造器构造一个函数,所以这里将会返回一个构造函数名字是Foo.getName
的函数(对象)。
6.new Foo().getName()
这里和上面又不一样了,需要这么看(new Foo()).getName()
,先调用Foo()
这个构造函数,实例化一个对象,然后调用这个对象的getName
方法,既然是实例化对象的方法,那当然应当是在原型链上面的,所以这里返回3
。
7.new new Foo().getName()
这里其实就是把5和6结合了起来,先打括号分割一下new (new Foo()).getName()
,同样的先是实例化了一个对象出来,然后再对这个实例化对象的getName
方法使用new
,这个结果和第5是一样的,返回一个构造函数名字是Foo.getName
的函数(对象)。
作用域
1. 外层变量在内部可以找到,反之找不到
以下看个案例:
var a=10;
function aaa(){
alert(a);
}
function bbb(){
var a=20;
aaa();
}
bbb();
结果是?
aaa()和外层的a=10处于同一个变量作用域。所以只能查找10.
如果把a=10去掉呢?结果是报错了。
还是变量作用域的问题,aaa的环境在全局环境下,不可能找到bbb里面的a=20.
2. var的问题:不var就是全局变量
var不写也是可以的。但是不写可能产生一些问题——变量会变成一个全局变量!
由此衍生出这样的问题:
function aaa(){
var a=b=10;
}
aaa();
alert(a);
alert(b);
回想问题,b变成了一个全局变量,a是一个局部变量。
所以alert(a)会报错。
去掉alert(a),aaa()运行的结果是产生了一个全局变量b和一个局部变量a。所以alert(b)的结果是10。
3.变量查找是就近原则,寻找var定义的变量
var a=10;
function aaa(){
alert(a);
var a=20;
}
aaa();
结果是什么?
既不是10也不是20。是undefined
当就近未找到,就会查找外层。一层一层知道直到找到为止。把结果简化一下吧:
var a=10;
function aaa(){
var a=20;
alert(a);
}
aaa();
结果是20。这很符合常识的推断。
var a=10;
function aaa(){
a=20;
alert(a);
}
aaa();
弹出a也是20,但是运行过程需要注意:最开始查找10,接着查找内部,发现a被修改为20.——本质是调用了外层的a。
好了。现在回到本节最初的例子,aaa调用alert的时候,查找的是外层的a,这个进程遇到var a=20这一步时,由于函数内部预解析,查找的作用域就变了。开始查找函数内部的a,但是var a=20这一步放在了后面,所以结果是undefined。
实际上代码变成了这样:
var a=10;
function aaa(){
var a;
alert(a);
a=20;
}
aaa();
所以所有变量在定义时必须放最前面、
4.结合分析
var a=10;
function aaa(){
bbb();
alert(a);
function bbb(){
var a=20;
}
}
aaa();
的结果是什么?
弹出结果是10。因为a是局部中的局部,说白了找不着。
那如果这么写呢?
var a=10;
function aaa(){
bbb();
alert(a);
function bbb(){
return a=20;
}
}
aaa();
好了。由于函数的预解析作用,导致bbb执行修改了全局变量a。所以弹出结果是20.同理,如果我把全局变量var a=10
删去。结果还是20。因为bbb又创造了一个全局变量a。
5. 参数跟局部变量同名,优先级是等同的
var a=10;
function aaa(a){
alert(a);
}
aaa(a);
这个简单的例子中弹出的是10。
如果把参数改成b,
var a=10;
function aaa(b){
alert(a);
}
aaa();
结果还是10.
那么这个a究竟是参数呢,还是全局变量a?
答案是,参数名和全局变量名一样时,走的是参数,不一样时,走的是全局变量。
比如:
var a=10;
function aaa(a){
a+=3;
}
aaa(a);
alert(a);//结果是10
基本数据类型不存在引用关系,里面的a就是一个局部变量
看个纠结一点点的吧:
var a=5;
var b=a;
b+=3;
alert(a)
a是5。这是常识。
基本类型的赋值是不存在引用关系的。但如果我想让a和b存在引用关系,应该怎么做?
var a={
a:5
}
var b=a;
b.a+=3;
console.log(a);//{a:8}
如果不存在依存关系——
var a={
a:5
}
var b={
a:a.a
};
b.a+=3;
console.log(a);
另外一个简单例子是数组。
假设这样
var a=[1,2,3];
var b=a;
b.push(4);
console.log(a);//[1,2,3,4]
所以真的要小心了。如果你不想存在引用关系,应该
var b=a.slice()
复合对象应用关系还可以衍生出这样的操作:
var a=[1,2,3];
function aaa(a){
a.push(4);
}
aaa(a);
console.log(a);//[1,2,3,4]
真是没有做不到,只有想不到,这个a又是什么鬼?不是局部变量吗?
a是确实是局部变量没错,但是它引用了外部的数组a。对这个局部变量的操作必将导致外部a的改变!
下面的代码或许让人更清醒些——
var a=[1,2,3];
function aaa(a){
a=[1,2,3,4]
}
aaa(a);
console.log(a);//[1,2,3,4]
因为参数a没有引用外部的a,所以怎么操作都跟外部的a没半毛钱关系!
补充:声明提前
声明提前,即在执行JS程序之前,会将所有的变量声明以及函数声明“提前”至其作用域的顶部。
对于变量只“提前”声明,不涉及赋值。
对于函数声明会将函数名称和函数体都“提前”,这一过程也成为“预编译”或“预解析”。
1.变量生命提前
<script> // console.log(word) // var word="hello"; // 打印结果为undfined // 变量声明提前 // 相当于 var word console.log(word) word="hello" </script>
2.函数声明提前(也称函数预解析)
首先有三种定义函数的方式
1.函数声明
//函数声明的语法 function fn (){ console.log("这是函数声明的方式"); } fn();//直接调用函数名
2.函数声明表达式(也叫匿名函数)
var fn = function([参数列表]){ console.log("这是一个匿名函数"); } /* 顾名思义,因为这种写法是把函数赋给了一个变量,函数并没有真正的名字 因此就叫匿名函数 */ fn();//调用匿名函数,使用变量名称加();
3.自执行函数
//顾名思义 //,自己执行自己,并且在声明的同时就调用自己,只能调用一次 //有两种写法,先写第一种: (function(){ console.log("这是一个自执行函数"); })();
函数声明提前
函数声明提前是整个函数体的提前
<script> a() function a(){ console.log('执行') } // 相当于 // function a(){ // console.log('执行') // } // a()
</script>
进一步例子
<script> // 变量声明提前 console.log(typeof a); // undefined var a = 1; console.log(typeof a); // number // 变量声明执行过程提前相当于: var a; console.log(typeof a); // undefined a = 1; console.log(typeof a); // number // 函数声明提前 console.log(typeof a); // function function a(){} console.log(typeof a); // function </script>
足可以证明,这一结论;假如不是整体提升,是类似于 var变量声明的提升的方式,应该会如 var变量声明的方式的输出方式相同, 先输出 undefined,再输出 function;
函数声明赋值和变量声明赋值的优先级
函数声明先赋值,变量声明执行到赋值语句才赋值
<script> console.log(typeof a); // function var a = 1; function a(){} console.log(typeof a); // number </script>
通过上边的, 明显可以看出来,应该 函数声明先赋值的,它是在执行上下文的执行阶段一开始的时候就已经进行了赋值操作,所以 最开始 typeof a 可以得到 function;而,变量声明 是要执行到赋值语句的时候才进行的赋值,所以 最后 typeof a 得到是 number;
当变量名和函数名相同时,函数声明先赋值,变量声明到 var a=1的时候,a已经赋值给变量了,不再是函数,a() 打印结果 a is not a function
总结
1.函数声明 和 变量声明都有声明提升机制
2.函数声明 是整体提升的方式,来进行的声明提升;
3.函数声明 和 变量声明,无所谓谁优先于谁,都是在预编译阶段进行的;(根据第4点的总结,也可以理解为 函数声明优先于变量声明)
4.函数声明赋值 要早于 变量声明赋值
函数声明赋值,是在执行上下文的开始阶段进行的;
变量声明赋值,是在执行到赋值语句的时候进行的赋值;