Js代码分为两个阶段:编译阶段和执行阶段
Js代码的编译阶段会找到所有的声明,并用合适的作用域将它们关联起来。
包括变量声明(var a)和函数声明(function a(){})在内的所有声明都会在代码被执行前的编译阶段首先被处理。这个过程就好像变量声明和函数声明从他们代码中出现的位置被添加到最近执行环境的顶部,这个过程就叫做提升(hoisting)。
只有声明操作会被提升,赋值和逻辑操作会被留在原地等待执行。
变量的声明
js中,在使用一个变量之前应当先声明。变量是使用关键字var声明的,如:
var i;
var sum;
注:此时console.log(i)=undefined,因为此时的变量没有被赋值,默认初始值是undefined。
也可以使用一个var关键字声明多个变量:
var i,sum;
而且还可以将变量的初始赋值和变量声明合写在一起:
var m="hello";
var i = 0,j = 0,k = 0;
到这里你大概会发现js的变量声明中没有指定数据类型,就比如在C语言中,我们定义一个变量常常是int a;char c;。。。。这里的int char 就是所谓的数据类型
小知识:编程语言分为动态(类型)语言和静态(类型)语言,动态类型语言是指在运行期间才会去做数据类型检查的语言,也就是说,在用动态类型的语言编程的时,永远也不用给任何变量指定数据类型,该语言会在第一次赋值给变量时,在内部将数据类型记录下来。静态类型语言与动态类型语言刚好相反,它的数据类型是在编译期间检查的,也就是说在写程序时要声明所有变量的数据类型。
静态类型语言:C/C++、C#、JAVA等
动态类型语言:Python、Ruby、JavaScript等
变量声明提升
举个例子:
var a = 1;
var b =2;
上述两行代码在编译时,实际上它的顺序是这样的:
var a; //undefined
var b; //undefined
a = 1; //1
b = 2; //2
解释:编译器将var a = 1;这行代码分成两步操作,
第一步是变量的声明提升:var a;声明操作会在编译阶段进行,所有声明会被提升到最近执行环境的顶部,此时的变量值是初始值undefined。
第二步是常规的赋值操作:a = 0;
这里有一点要注意,JavaScript中声明变量可以不写前面的var关键字,这种是合法且可行的,去掉var时,会被默认成全局变量,但是我们不建议这样使用,这个不好的习惯会造成很多bug。严格模式下,不声明就使用一个变量也会导致错误。比如:
function add(num1,num2){
var sum = num1 + num2;
return sum;
}
var result = add(10,20); //30
alert(sum); //由于sum在函数体内被声明过,在全局不是有效的变量,因此会导致错误
改成:
function add(num1,num2){
sum = num1 + num2;
return sum;
}
var result = add(10,20); //30
alert(sum); //30 此时的sum是全局变量,所以这里可以访问的到。但是不推荐。
函数的声明
定义函数有三种方式:函数声明、函数表达式、Function构造函数(不推荐)
函数声明提升会在编译阶段把声明和函数体整体都提前到执行环境顶部,所以我们可以在函数声明之前调用这个函数。比如:
function sum(num1,num2){
return num1 + num2;
}
函数表达式,其实就是变量声明的一种,声明操作会被提升到执行环境顶部,并赋值undefined。赋值操作被留在原地等到执行。这种定义方式得到的函数也叫匿名函数(拉姆达函数),因为function关键字后面没有函数名字,只是把这个函数体赋值给一个变量。这种方式定义函数也没有必要使用函数名---通过变量名就可以引用函数。另外还要注意,此时函数末尾有一个分号,就像声明其他变量一样需要一个分号作为结尾。比如:
var sum = function (num1,num2){
return num1 + num2;
};
Function构造函数可以接收任意数量的参数,但最后一个参数始终都被看成函数体,而前面的参数枚举出了新函数的参数。比如:
var sum = new Function("num1","num2","return num1 + num2"); //不推荐
从技术上讲,这是一个函数表达式,但是这种语法会导致解析两次代码(第一次是解析常规的ES代码,第二次是解析传入构造函数中的字符串),从而影响性能。不过,这种语法对于理解“函数是对象,函数名是指针”的概念倒是非常直观的。
函数的声明提升
小知识:在一些类似C语言的编程语言中,花括号内的每一段代码都是具有各组的作用域,而且变量在声明它们的代码段之外是不可见的。我们称之为块级作用域(block scope),然而,JavaScript是没有块级作用域。JavaScript取而代之地使用了函数作用域(function scope):变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。
function test(0){
var i = 0; //i在整个函数体内均有定义
if(typeof 0 == "object"){
var j = 0; //j在整个函数体内均有定义,不仅仅是在这个代码段内
for(var k = 0;k < 10;k++){ //j在整个函数体内均有定义,不仅仅是在这个循环内
console.log(k); //输出数字0--9
}
console.log(k); //k已经定义了,输出10
}
console.log(j); //j已经定义了,但可能没有初始化
}
JavaScript的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的,所以当在函数体内使用各种for嵌套循环时应定义不同的变量i,j,k等。
举个例子:
var scope = "global";
function f(){
console.log(scope); //undefined
var scope = "local"; //变量在这里赋值,但是在整个函数体内都是有定义的
console.log(scope); //local
}
你可能会认为第一个console会输出“global”,但是由于函数作用域的特性,局部变量在整个函数体内都是有定义的,上述代码等价于:
var scope = "global";
function f(){
var scope; //undefined,因为函数声明优先级更高,所以覆盖了第一行的赋值
console.log(scope); //变量存在,但其值是初始值undefined
scope = "local"; //变量在这里赋值为local
console.log(scope); //local
}
最后总结,声明的顺序是这样的:
第一步. 找到所有的函数声明,初始化函数体,如有同名的函数则会进行覆盖
第二步. 查找变量声明,初始化为undefined,如果已经存在同名的变量,就什么也不做直接略过。
要注意咯,函数声明的优先级比变量声明的优先级高,如果是先声明并赋值了一个变量,后面的函数体内又声明了同名变量,此时的变量就被函数体内的变量覆盖了。