您的位置:首页 > Web前端 > JavaScript

ES 5、ES 6变量和函数声明以及作用域总结

2016-01-20 15:37 555 查看

变量提升

JavaScript引擎在执行的时候,会把所有变量的声明都提升到当前作用域的最前面。当前作用域内的声明都会提升到作用域的最前面,包括变量和函数的声明。

var v = "hello";
(function(){
console.log(v);
var v = "world";
})();


这段代码运行的结果是:undefined

第一,function作用域里的变量v遮盖了上层作用域变量v。

var v = "hello";
if(true){
console.log(v);
var v = "world";
}


代码做少些变动,输出结果为”hello”,说明ES 5是没有块级作用域的。函数是ES 5中唯一拥有自身作用域的结构。

第二,在function作用域内,变量v的声明被提升了。所以最初的代码相当于:

var v = "hello";
(function(){
var v; //declaration hoisting
console.log(v);
v = "world";
})();


(function(){
var a = "1";
var f = function(){};
var b = "2";
var c = "3";
})();


变量a,f,b,c的声明会被提升到函数作用域的最前面,类似如下:

(function(){
var a,f,b,c;
a = "1";
f = function(){};
b = "2";
c = "3";
})();


下面的代码中函数声明f2被提升,所以在前面调用f2是没问题的。虽然变量f1也被提升,但f1提升后的值为undefined,其真正的初始值是在执行到函数表达式处被赋予的。所以只有声明是被提升的。

(function(){
//var f1,function f2(){}; //hoisting,被隐式提升的声明

f1(); //ReferenceError: f1 is not defined
f2();

var f1 = function(){};
function f2(){}
})();


ES5 有var和没var声明

ES5中只有全局作用域函数作用域,我们依次分析。

函数作用域

var i=100;  //显示申明

i=100;  //隐式申明


在函数内部,有var和没var声明的变量是不一样的。在函数中使用var关键字进行显式申明的变量是做为局部变量,而没有用var关键字,使用直接赋值方式声明的是全局变量,所以可以借此向外暴露接口。

JavaScript中变量声明有var和没var的区别,所以ES5中经常见到避免全局变量污染的方法是匿名立即执行函数:

(function(){
// ...
})();


全局作用域

在全局作用域内声明变量时,有var 和没var看起来都一样,声明的全局变量,就是window的属性。但其实还是有差别的,我们用delete删除属性来验证下,配置性为false的属性无法删除。也就是通过变量var声明全局对象的属性无法删除,我们还会发现和函数声明创建的全局对象属性也无法删除。

var fff = 2;
window.ffa = 3;
ffb = 4;
this.ffc = 4;
var ffftx = Object.getOwnPropertyDescriptor(window, 'fff'); //configurable:false,enumerable:true,value:2,writable:true
var ffatx = Object.getOwnPropertyDescriptor(window, 'ffa'); //configurable:true,enumerable:true,value:2,writable:true
var ffbtx = Object.getOwnPropertyDescriptor(window, 'ffb'); //configurable:true,enumerable:true,value:2,writable:true
var ffctx = Object.getOwnPropertyDescriptor(window, 'ffc'); //configurable:true,enumerable:true,value:2,writable:true


delete fff; // 无法删除
delete ffa; // 可删除
delete ffb; // 可删除
delete ffc; // 可删除


使用var声明变量,是在当前域中声明变量. 如果在方法中声明,则为局部变量;如果是在全局域中声明,则为全局变量。而num = 1,事实上是对属性赋值操作。首先,它会尝试在当前作用域链(如在方法中声明,则当前作用域链代表全局作用域和方法局部作用域etc。。。)中解析 num; 如果在任何当前作用域链中找到num,则会执行对num属性赋值; 如果没有找到num,它才会在全局对象(即当前作用域链的最顶层对象,如window对象)中创造num属性并赋值。

注意!没有用var关键字,本质上并不是声明了一个全局变量,而是创建了一个全局对象的属性。

函数定义两种方式的区别

函数定义有两种方式,一种是函数定义表达式,一种是函数声明语句。

// 函数定义表达式
var fns = function (){
// ...
};
// 函数声明语句
function fns(){
// ...
}


由于声明是会被提前的,所以函数声明语句“被提前”到外部脚本或外部函数作用域的顶部,所以以这种方式声明的函数,可以被再它定义之前出现的代码所调用。而函数定义表达式中,变量的声明被提前了,但是给变量的赋值是不会提前的,所以,以表达式方式定义的函数在函数定义之前无法调用。

(function() {
testa();            // 打印出testa
testb();            // 报错:提示undefined is not a function
console.log(testc); //undefined,如果移到上面就可以了
function testa() {
console.log("testa");
}
var testb = function() {
console.log("tesb");
}
var testc = "testc";
})();


ES 6对块级作用域的支持

ES 5没有块级作用域

,经常容易遇到两类问题。

内层变量可能会覆盖外层变量。

var tmp = new Date();

function f(){
console.log(tmp);
if (false){
var tmp = "hello world";
}
}

f() // undefined


上面代码中,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。

用来计数的循环变量泄露为全局变量。

var s = 'hello';

for (var i = 0; i < s.length; i++){
console.log(s[i]);
}

console.log(i); // 5


ES 6对块级作用域的支持

上面的函数有两个代码块,都声明了变量n,运行后输出5。这表示外层代码块不受内层代码块的影响。如果使用var定义变量n,最后输出的值就是10。

function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}


ES6允许块级作用域的任意嵌套。外层作用域无法读取内层作用域的变量。内层作用域可以定义外层作用域的同名变量。

{{{{
{let insane = 'Hello World'}
console.log(insane); // 报错
}}}};


{{{{
let insane = 'Hello World';
{let insane = 'Hello World';}
}}}};


ES6也规定,函数本身的作用域,在其所在的块级作用域之内。

function f() { console.log('I am outside!'); }
(function () {
if(false) {
// 重复声明一次函数f
function f() { console.log('I am inside!'); }
}

f();   //  *
}());


上面代码在ES5中运行,会得到“I am inside!”,但是在ES6中运行,会得到“I am outside!”。这是因为ES5存在函数提升,不管会不会进入 if代码块,函数声明都会提升到当前作用域的顶部,得到执行;而ES6支持块级作用域,不管会不会进入if代码块作用域,其内部声明的函数皆不会影响到作用域的外部,即*所在行的函数不会受if代码块内部的影响。

块级作用域外部,无法调用块级作用域内部定义的函数。如果确实需要调用,就要像下面这样处理。值得注意的是,有的浏览器可能并不认为{ }形成了一个块级作用域。

{
let a = 'secret';
function f() {
return a;
}
}
f() // 报错


如果确实需要调用,就要像下面这样处理。

let f;
{
let a = 'secret';
f = function () {
return a;
}
}
f() // "secret"


var和let、const的区别

作用域

let和const命令所声明的变量,只在let命令所在的代码块内有效。

{
let a = 10;
var b = 1;
}

a // ReferenceError: a is not defined.
b // 1


for循环的计数器,就很合适使用let命令。

for(let i = 0; i < arr.length; i++){}

console.log(i)
//ReferenceError: i is not defined


用var声明的变量i,在全局范围内都有效。所以每一次循环,新的i值都会覆盖旧值,导致最后输出的是最后一轮的i的值。

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是6。

var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10


用let声明的变量i,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。

var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6


const命令声明的变量不允许修改

对于复合类型的变量,变量名不指向数据,而是指向数据所在的地址。const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以将一个对象声明为常量必须非常小心。

const foo = {};
foo.prop = 123;

foo.prop
// 123

foo = {} // TypeError: "foo" is read-only


上面代码中,常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

const a = [];
a.push("Hello"); // 可执行
a.length = 0;    // 可执行
a = ["Dave"];    // 报错


如果真的想将对象冻结,应该使用Object.freeze方法。

const foo = Object.freeze({});

// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
foo.prop = 123;


上面代码中,常量foo指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。

除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

var constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, value) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
});
};


变量提升

let和const没有“变量提升”现象。所以,变量一定要在声明后使用,否则报错。

console.log(foo); // 输出undefined
console.log(bar); // 报错ReferenceError

var foo = 2;
let bar = 2;


重复声明

使用var语句重复声明语句是合法且无害的。如果重复声明且带有赋值,那么就和一般的赋值语句没差别。如果尝试读取没有声明过的变量,Js会报错。

但是ES 6中新增的let、constlet不允许在相同作用域内,重复声明同一个变量。

// 报错
function () {
let a = 10;
var a = 1;
}

// 报错
function () {
let a = 10;
let a = 1;
}


因此,不能在函数内部重新声明参数。

function func(arg) {
let arg; // 报错
}

function func(arg) {
{
let arg; // 不报错
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息