一:变量作用域问题:
var a = 1; //此处声明的变量a为全局变量
function foo(){
var a = 2;//此处声明的变量a为函数foo的局部变量
console.log(a);//2
}
foo();
console.log(a);//1
复制代码
var a = 1;
console.log(a); // 1
if (true) {
var a = 2;
console.log(a); //2
}
console.log(a);// 2
复制代码
为何两段代码输出的不一样???
因为::JavaScript只有全局作用域和函数级作用域,没有块级作用域
。所以在第一段代码中的var a=2 只在函数foo()中起作用,并不会污染外部的全局变量。而if代码块不会创建新的作用域,所以在第二段代码中的if语句块中,会污染全局变量a,从而改变了a的值。
【而在C语言或者Java语言中,if等块级作用域也会声明新的变量,不会污染外部的全局变量】
如果在函数作用域内声明变量时省略了var 那么该变量就会变为全局变量,从而污染全局变量的值。如:
var a = 1; //此处声明的变量a为全局变量
function foo(){
a = 2;//此处的变量a也为全局变量
console.log(a);//2
}
foo();
console.log(a);//2
复制代码
那如何解决函数作用域内即不污染外部公共变量,又不污染函数作用域内的变量呢?
那就是在函数作用域内加个临时作用域
var x = 3;
function foo() {
var x = 1;
console.log(x); //foo作用域变量x,输出1
(function () {
var y = 2;
console.log(x); //foo作用域变量x,输出1
console.log(y); //临时作用域变量y,输出2
})();
console.log(y); //临时作用域变量y只在临时作用域内有效,提示 y is not defined
}
foo();
console.log(x); //全局变量x,输出3
复制代码
此时,又出现了新的问题,临时作用域
变量y在临时作用域外部无法获取,如何解决这个问题呢?
var x = 3;
function foo() {
var x = 1;
console.log(x); //foo作用域变量x,输出1
var temporary_res = (function () {
var y = 2;
return y;
})();
console.log(temporary_res); //接收的临时作用域变量y,输出2
}
foo();
console.log(x); //全局变量x,输出3
console.log(temporary_res); //只在foo函数内有效,此处提示 temporary_res
复制代码
二:var变量提升问题:
var v='Hello World';
(function(){
console.log(v);
})()
复制代码
这种情况下,是可以打印出Hello World的。
下面,我们稍加改造:
var v='Hello World';
(function(){
console.log(v);
var v='I love you';
})()
复制代码
这时,就会提示undefined,
这是为什么呢?
因为在临时作用域内多加了这么一个同名变量v的声明赋值语句,var v = ‘I love you’;
那么,本着块级作用域
不会污染外部全局变量的原则,只要作用域内加了同名变量的声明
,那么该声明的变量只会在该作用域内有效,外部的同名全局变量在本作用域内就失效了。
不是很理解的话可以看看下面这段代码打印输出的结果。
var x = 3;
function foo() {
var x = 1;
console.log(x); //foo作用域变量x,输出1
(function () {
var x = 2;
console.log(x); //临时作用域变量x,输出2
})();
console.log(x); //foo作用域变量x,输出1
}
foo();
console.log(x); //全局变量x,输出3
复制代码
那么,上面的代码我们再来改造一下,临时作用域内只给x赋值为2,不做变量声明,那么,此时在临时作用域内的x=2就代表着把foo作用域变量x的值重新赋值为2。
var x = 3;
function foo() {
var x = 1;
console.log(x); //foo作用域变量x,输出1
(function () {
console.log(x); //foo作用域变量x,输出1
x = 2;
console.log(x); //改变了foo作用域x的值,输出2
})();
console.log(x); //foo作用域x的值,输出2
}
foo();
console.log(x); //全局变量x,输出3
复制代码
如果此时块级作用域内声明并赋值的变量不是v,而是其他的变量名,那么,这时运行代码就不会提示undefined了。
var v='Hello World';
(function(){
console.log(v);
var a='I love you';
})()
复制代码
深层次的解释一下:
全局声明变量赋值 var x = 1;
作用域内声明变量赋值 var x = 2;
这相当于在内存地址中开辟了两块存储空间,虽然两者同名,但是在内存中却是不同的存储地址,所以在代码调用时,实际上是根据不同的内存地址找到的对应的值。
如果在局部作用域中不声明变量而是直接写x=2,那就是把全局变量x内存地址中的值改为了2。
那么,这样表面上看是多了一句 var v=’I love you’; 是在临时作用域内做变量声明并赋值,实际上就是在内存中新开辟了一块栈存储空间,并进行赋值。
那这样能理解明白的话,那么临时作用域中的console.log打印的v就不难理解了,就是在内存中新开辟的存储空间的值。
其实这里还隐藏着变量提升的问题。
var v=’I love you’; 这句代码可以拆分成两部分,一是声明变量,二是给变量赋值。
拆分之后如下:
var v='Hello World';
(function(){
var v;
console.log(v);
v ='I love you';
})()
复制代码
所以才会提示undefined。因此,我们在写代码时就应该养成变量声明赋值应放在作用域顶部,防止出现上面类似的问题而一头雾水。
三:函数提升
声明函数方式可以进行函数提升
function test(){
function foo(){
alert("我来自 foo");
}
foo();
}
test();
复制代码
function test(){
foo();
function foo(){
alert("我来自 foo");
}
}
test();
复制代码
后面这个方法就存在函数提升,提升后就是上面的写法。
函数表达式
方式就不能进行函数提升了。
function test(){
foo();
var foo =function foo(){
alert("我来自 foo");
}
}
test();
复制代码
正确的写法应该是:
function test(){
var foo =function foo(){
alert("我来自 foo");
}
foo();
}
test();
复制代码
感谢浪迹天涯博主分享的文章
四:ES6 let
其实,在ES6(ES2015)出现之前,JavaScript声明变量只有两种方式,那就是var和function,可是,上面出现的那些问题,很多时候对于刚入职甚至入职很久的朋友来说都很难避免出现失误,所以在实际工作中踩的坑多,总结的经验就多。别人踩过的坑总结的经验我们可以学习借鉴。
ES6之后,声明变量增加了let、const,而且ES6标注的很明确,let、const声明的变量只在本作用域内有效,这样的话,我们在ES6开发中只要记住使用let、const声明的变量只在本作用域内有效就行了,而且同一个作用域内不能重复声明变量。如果作用域内的变量没有使用let声明而是直接赋值,那说明就是将作用域外部的全局变量更新值。这样记起来多方便。
下面来看一个经典案例。
for (let i = 0; i < 10; i++) {
setTimeout(function(){
console.log(i);
},100)
};
console.log(i);
复制代码
因为let i是在for循环内部定义的,所以i的作用域范围只能是for循环内部,外部打印i时就提示 i is not defined。for循环内部每一次循环其实都是一个新的变量i,所以打印从0到9的数字。至于为什么先显示外部的i找不到,是因为setTimeout延时造成的。
for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
复制代码
上面代码正确运行,输出了 3 次abc
。这表明函数内部声明的变量i
与循环体变量i
不在同一个作用域,有各自单独的作用域(同一个作用域不可使用 let
重复声明同一个变量)。
如果for循环内的i直接赋值为abc
的话,那么,此时就是改变的for循环体内i
的值,那么console也只能打印一次abc出来。
for (let i = 0; i <= 3; i++) {
i = 'abc';
console.log(i);
}
复制代码
for (let i = 0; i < 3; i++) {
console.log(i);
let i = 'abc';
console.log(i);
}
复制代码
如果在for循环体内的let i声明变量之前打印i时,就是提示初始化前无法访问i。这样也更加证明了函数内部的变量i
跟循环体变量i
不在同一个作用域。
for (let i = 0; i <= 3; i++) {
console.log(i);
i = 'abc';
console.log(i);
}
复制代码
如果循环体内的i
是直接赋值,那么,此时打印出来的就是0和abc。
再来看一个很有迷惑性的for循环代码
var a = [];
var b = [];
for (var i = 0; i < 10; i++) {
console.log(i); //打印 0-9
a[i] = function () { //数组下标 0-9,每次循环的时候,都会给全局变量a赋值不同的下标i。 a[i]实际就是一个函数表达式
console.log(i); //这里不要被迷惑了,因为i是全局变量,虽然每次循环时,这里的i是0-9的数字,但是,这里的方法不会立即执行,i作为全局变量是一直累加的,直到循环体i++ 后等于10不满足循环条件i<10 才停止下来,所以,全局变量i最后的值是10
};
b[i]=i; //每次循环时都给全局变量b赋值不同的下标i,对应下标也赋值不同的值i
}
a[6](); //这里实际就是执行函数表达式,打印全局变量i,而通过for循环后,全局变量i最后的值是10,所以此处打印值为10
console.log(b[6]); //b[i]在循环体内每次被赋值后作为全局变量b的子内容被保存下来,所以这里打印值为6
复制代码
五:ES6 let不存在变量提升
// var 的情况
console.log(a); // 输出undefined
var a = 2;
// let 的情况
console.log(b); // 报错ReferenceError【引用错误】
let b = 2;
复制代码
六:ES6 暂时性死区和块级作用域
ES6 明确规定,如果区块中存在let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
下面我们把文章一开头的代码段使用let进行变量声明来看一下打印结果:
let a = 1;
console.log(a); // 1
if (true) {
let a = 2;
console.log(a); //2
}
console.log(a);// 1
复制代码
同样的代码,使用var进行变量声明时,最后打印的结果是 1 2 2。
var a = 1;
console.log(a); // 1
if (true) {
var a = 2;
console.log(a); //2
}
console.log(a);// 2
复制代码
这里就说明了if代码块中使用let 重新声明a变量时,if区块内是该新声明的变量a的作用域,出了这个区域就失效了。
而使用var声明变量时,if区块就不会作为有效区域了,只有在函数内声明
才能作为有效作用域。
注意:ES6 的块级作用域必须有大括号,如果没有大括号,JavaScript 引擎就认为不存在块级作用域。
// 第一种写法,报错
if (true) let x = 1;
// 第二种写法,不报错
if (true) {
let x = 1;
}
复制代码
上面代码中,第一种写法没有大括号,所以不存在块级作用域,而let
只能出现在当前作用域的顶层,所以报错。第二种写法有大括号,所以块级作用域成立。
那么,这里的暂时性死区的举例代码又牵扯出了一个知识点。
七:ES6 let不允许变量重复声明。
var a = 1;
var a = 2;
console.log(a);// 2
复制代码
let a = 1;
let a = 2;
console.log(a);
复制代码
提示变量a被重复声明
八:使用var声明的循环变量泄露为全局变量
示例:
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i); // 5
复制代码
按说,当循环结束后,i应该被销毁,可是却被泄露成了全局变量,假如在for循环内部添加判断条件进行变量赋值等,也应该是在for循环外提前定义好变量,在for循环内部根据判断条件赋值后,在for循环结束后使用重新赋值的变量,而不是使用i,这样非常容易造成污染全局变量。
九:ES6 const常量声明
const
声明一个只读的常量。一旦声明,常量的值就不能改变。
const PI = 3.1415;
PI = 3;
复制代码
上面代码表明改变常量的值会报错,说明const
声明的变量不得改变值。
const
一旦声明变量,就必须立即初始化,不能留到以后赋值。
const
的作用域与let
命令相同:只在声明所在的块级作用域内有效。
if (true) {
const MAX = 5;
}
MAX // Uncaught ReferenceError: MAX is not defined
复制代码
const
声明的常量也不能提升,同样存在暂时性死区,只能在声明的位置后面使用。
if (true) {
console.log(MAX); // ReferenceError
const MAX = 5;
}
复制代码
const
声明的常量,也与let
一样不可重复声明。
var message = "Hello!";
let age = 25;
// 以下两行都会报错
const message = "Goodbye!";
const age = 30;
复制代码
const
实际上保证的并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。
但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const
只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。
因此,将一个对象声明为常量必须非常小心。
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
复制代码
上面代码中,常量foo
储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo
指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。
const a = [];
a.push('Hello'); // 可执行
a.length = 0; // 可执行
a = ['Dave']; // 报错
复制代码
上面代码中,常量a
是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a
,就会报错。
总结:
ES6 之前只有两种声明变量的方法:var
命令和function
命令。ES6 除了添加let
和const
命令,另外两种声明变量的方法:import
命令和class
命令。所以,ES6 一共有 6 种声明变量的方法。
上面已经对比了这么多的代码和示例,对于以后使用ES6还是ES5,应该自己有明确的判断了吧。