首先,我承认这篇文章是看了这篇文章后才写出来的。
迅雷面试题
今天在参加迅雷的面试的时候,第一道题就把我难住了,原题是这样:
// 请写出每一个alert的结果var a = 100; function fun() { alert(a); var a = 200; alert(a); } fun(); alert(a); var a; alert(a); var a = 200; alert(a);
从javascript的基础功力来看,瞬间我就被秒了一地。我当时只想到了变量的作用域,却没想到变量声明提升,所以当时是蒙圈儿的,乱来了。
变量的作用域
这在我上一篇文章已经讨论过了,这里就直接给出一个结论。javascript中,变量的作用域不是块级的,而是以function为单位。所谓块级,就是{}花括号括起来为一块,以function为单位,就是指变量的作用域上限就是当前所在的函数。
var a = 100; function fun() { var a = 5; alert(a); } alert(a); fun(); for(i = 0;i < 5;i ++) { var a = i; } alert(a);
上面这段代码中,用一个for后面的{}来演示块级区域,虽然在for{}中重新声明了a,但是实际上,这个a作用域仍然为全局环境,它该变了最初的a的值,因此,末尾的alert(a)结果为4。但是在fun(){}中,var a 的作用域是当前函数,因此,函数内的alert(a)是5。
ES6标准新增了let和const两种声明关键词,这样和var一起,就使得javascript有三种变量声明形式,每种声明形式声明的变量作用域不同,新标准不再局限于函数级。举个例子:
const a = 6;function fun() { var b = 9; for(let b = 2;b --;) { alert(b); } a = 5; alert(a); alert(b); } fun();a将永远是6;b则不同,在fun()内是9,但是当进入到for{}以后是2,退出for{}之后,又变回9。也就是说用const声明的变量不再是变量,而是常量,和PHP里面的define、const是一样的。let声明的变量只在当前块级区域内有效,也就是for{}的两个花括号范围内有效,出了花括号又无效了。
变量的声明提升
当对变量的声明出现在了相同作用域的靠后的位置的时候,变量的声明被自动提升到作用域开头。
我们一般在声明变量的时候,会这样操作:
var a = 100;
实际上,这个动作完成了三件事:声明一个变量,定义这个变量的数据类型,赋值(初始化)。
那么当这个动作出现在同一个作用域的靠后的位置,javascript会把代码解释为什么情况呢?
alert(a); var a = 100;
上面这段代码,实际上会被按照下面这个顺序解释:
var a; alert(a); a = 100;
所以,alert(a)的结果是undefined。所谓变量声明提升,就是被声明动作如果发生在靠后的位置,会被自动提升到作用域的最前面。下面来举例复杂的例子:
var a = 100; alert(a); var a = 200; alert(a); function fun2() { alert(a); var a = 3; } fun2(); alert(typeof a); var a = function() {} alert(typeof a);
上面这段代码中,出现了多个var a对a变量进行声明,这个时候,我们要去区分,每一个a的作用域,然后对每一次声明进行提升,最终得到的结果如下:
var a; var a; var a; // 最终会被合并为一个var a; a = 100; alert(a); a = 200; alert(a); function fun2() { var a; alert(a); a = 3; } fun2(); alert(typeof a); a = function(){} alert(typeof a);
这样,你就可以非常清楚的看到每一个alert出现的时候,a的内容了。
隐式提升
除了让我们能够直观的看清楚js的执行顺序外,变量声明提升还有什么用呢?你没有看到,是因为在上面的所有例子中,我们还有一种情况未举出,下面我们看这样的一段代码:
var foo; alert(typeof foo); function foo(){}
这时alert返回的结果是undefined还是其他什么呢?实际上,结果是function,为什么呢?因为function foo;声明将被提升,我们把代码解释为:
function foo; var foo; alert(typeof foo); foo = function(){}
实际上,使用function声明一个函数时,也发生了变量提升,函数名即为变量名,变量的数据类型被定义为function,这和普通的var声明不同,普通的var只做声明,没有定义数据类型。在上面这段解释代码中,var foo;和function foo;发生了冲突,因为前者没有定义foo的数据类型,后者定义了数据类型。在这种冲突的情况下,前面的所有声明会被后者覆盖。(var声明未对变量做任何定义,因此foo的数据类型还是function)
这种function被声明提升的形式被成为hoisting(隐式提升的声明)。因此,function a(){}和var a = function(){}是不同的,他们声明的方式不同,在声明时的数据类型就不一样,所以,下面的代码出现了奇怪的现象:
alert(typeof f1); // undefined alert(typeof f2); // function var f1 = function(){} function f2(){}
你需要尝试去解释上面这段代码,然后就可以清楚的找到原因了。这也是为什么我们在写javascript的时候,可以把所有需要用到的函数放到代码的末尾,而普通用var声明的变量必须在你用到它之前进行定义(以及赋值,因为在javascript中,第一次定义就是初始化,而只有在赋值的时候,才会被定义)。
另外,javascript中一个名字(name)以四种方式进入作用域(scope),其优先级顺序如下:
1、语言内置:所有的作用域中都有 this 和 arguments 关键字2、形式参数:函数的参数在函数作用域中都是有效的3、函数声明:形如function foo() {}4、变量声明:形如var bar;
下面举个例子:
function fun(a) { var b = 100; function go() {} ... }
当javascript运行遇到这一段代码时,会首先声明那些变量,然后再声明哪些变量呢?在javascript解释中,它们的顺序是这样的:
fun -> this,arguments -> a -> go -> b
在遇到fun(){}时,首先this,arguments这些javascript内部定义的关键字会在作用域中生效,然后,fun的形式参数a,也紧接着在作用域中声明,再接着就是作用域内的function声明的变量会被声明,最后才轮到普通的局部变量被声明。但是声明归声明,在其后面的运行过程中,这些声明还是可以被覆盖的,比如
function fun(a) { var b = 100; alert(typeof b); function b() {} } fun(1);
你可以看到alert的结果是number,按照上面提到的顺序,javascript解释时会把function b放在var b的前面,而var b = 100;经过了定义,因此数据类型也发生了变化。
隐示提升与加载调用的区别
在上面的案例中,我们举过一些例子,你会发现,在使用某个变量之前,该变量必须被定义,否则typeof就是undefined,但是我们来看下面这个例子:
<a href="javascirpt:" onclick="btn.clickCancel();">cancel</a> <script> var btn = { clickCancel : function(){ // ... }, // .... }; </script>
你会发现,即使在定义btn之前使用btn.clickCancel(),程序是仍然有效的。这是什么原因呢?实际上,在<a>标签的onclick属性中去调用btn并不冲突,<a>标签的onclick事件是在DOM加载完才生效,而DOM加载完时,所有的javascript声明和初始化都已经完成,btn变量已经在内存中存在了。
delete
在javascript中,我们使用delete来删除某一个变量或属性,比如:
a = 10; delete a; alert(a); // undefined
但是,那些变量或属性可以被delete删除呢?哪些又不可以呢?这是一个比较复杂的问题,可以阅读一下这篇文章。我自己总结下来,就是凡严格声明的变量不能被delete删除,非严格声明的可以删除。我们来通过演示了解:
// 变量 var a = 10; b = 12; delete a; delete b; alert(a); alert(b); function fun() { var a = 10; b = 12; delete a; delete b; alert(a); alert(b); } fun(); eval("var x = 1;"); alert(x); delete x; alert(x);
上面这段代码,我们用来演示一个变量被delete的情况,你会发现,凡是用var进行严格声明的,都不能被删除,没有var声明的,会被删除。什么叫严格声明呢?就是eval()中声明的var x不算严格声明,因此delete x;成功了。
// 删除函数 function fun() {} delete fun; alert(typeof fun); window.fun1 = function(){} alert(typeof fun1); delete fun1; alert(typeof fun1);
我们上面提到过,function fun会被提升,是一种严格的声明,而window.fun1会使fun1成为全局函数,但却不是严格的声明,可以被删除。
// 删除属性 var object = { name : 'xx00' } element = { name : 'xxxx' } delete object.name; delete element.name; alert(object.name); alert(element.name); function Cat() { this.name = 'tom'; this.age = 10; } Cat.prototype.weight = 12;Cat.prototype.sing = 'wawa~'; var cat1 = new Cat(); delete cat1.name; delete cat1.weight; alert(cat1.name); alert(cat1.weight);
在我们以前的文章中讲过,javascript一切都是对象,因此,其实我们很多问题都要回归到对象这个核心概念来看。在这段演示代码中,object.name和element.name都被删掉了,无论object前面是否用var加以声明,都不会对object.name产生什么影响。其实这是比较容易理解的,对object的声明会让object无法被delete,而object的属性并并没被严格声明,而且在javascript里面并没有类似 object = {var name : 'xxx'}这样的写法,所以,可以说,单纯的对象属性,大部分都是可以被删除的,而唯独通过prototype赋予(声明)的属性,不能被删除。
现在再重新来思考
b = 10; window.fun = function(){}; object.name;
你就会发现,但凡没有声明的变量,实际上都是某个对象的属性,比如这里的b = 10;实际上就是window.b = 10,未经声明的属性都可以被删除。
注意:ES6带来了一些新的变化,严格模式"use strict"下,变量不声明会报错,程序终止;delete不能再删除变量,因此也会报错,终止程序。
2016-04-17 4245