var
var声明的变量是函数作用域,会定义到全局对象上
var a; // undefined 如果未指定初始值,初始值就是
var i = 0, j = 0, k = 0;
for (var p in o) console.log(p);//定义在循环体内使用的变量,如果用 var 定义,其实作用域在外部,全局或是函数
在函数体内定义的 var变量是局部变量
function test() {
var message = "hi"; // local variable
}
test();
console.log(message); // error!
没有用 var 定义的变量是全局变量
function test() {
message = "hi"; // global variable
}
test();
console.log(message); // "hi"
var定义的变量存在变量提升,根预解析有关。
在执行代码前,预解析,把所有 var 定义的变量放到词法作用域里面的变量环境,设置为 undefined。可以重名
如果我们仍然想要使用var,我的个人建议是,把它当做一种“保障变量是局部”的逻辑
- 声明同时必定初始化;
- 尽可能在离使用的位置近处声明;
- 不要在意重复声明。
var x = 1, y = 2;
doSth(x, y);
for(var x = 0; x < 10; x++)
doSth2(x);
改成
{
let x = 1, y = 2;
doSth(x, y);
}
for(let x = 0; x < 10; x++)
doSth2(x);
let
let声明的变量绑定到块作用域。不会成为全局对象的属性
不能重复声明,第一次声明的时候是 uninitial,存在执行上下文的词法作用域的词法环境
隐式的绑定
var foo = true;
if (foo) {
let bar = foo * 2;
bar = something( bar ); console.log( bar );
}
console.log( bar ); // ReferenceError
显式的绑定
if (foo) {
{
// <-- 显式的快
let bar = foo * 2;
bar = something( bar ); console.log( bar );
} }
console.log( bar ); // ReferenceError
垃圾回收
使用let 定义的变量,绑定到块作用域,其他作用域访问不到,
方便垃圾回收
function process(data) {
// 在这里做点有趣的事情
}
// 在这个块中定义的内容可以销毁了! {
let someReallyBigData = { .. };
process( someReallyBigData );
}
var btn = document.getElementById( "my_button" );
btn.addEventListener( "click", function click(evt){
console.log("button clicked");
}, /*capturingPhase=*/false );
块级作用域与函数声明
考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。
const
const声明一个只读的常量。一旦声明,常量的值就不能改变。
如果是对象,对象的地址不能变,属性可以增加修改
class
class最基本的用法只需要class关键字、名称和一对大括号。它的声明特征跟const和let类似,都是作用于块级作用域,预处理阶段则会屏蔽外部变量。TDZ
需要注意,class默认内部的函数定义都是strict模式的。
私有变量
严格来讲,JavaScript 中没有私有成员的概念;所有对象属性都是公有的。不过,倒是有一个私有 变量的概念。任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数的外部访问这些变量。 私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。
如果在这个函数内部创建一个闭包,那么闭包通过自己的作用域链也可以访 问这些变量。而利用这一点,就可以创建用于访问私有变量的公有方法。
第一种是在构造函数中定义特权方法
function Person(name){
this.getName = function(){ return name;
};
this.setName = function (value) { name = value;
}; }
var person = new Person("Nicholas"); alert(person.getName()); //"Nicholas" person.setName("Greg"); alert(person.getName()); //"Greg"
构造函数模式的缺点是针对每个实例都会创建同样一组新方法
静态私有变量
(function(){
//私有变量和私有函数
var privateVariable = 10;
function privateFunction(){
return false;
}
//构造函数
MyObject = function(){ };
//公有/特权方法
MyObject.prototype.publicMethod = function(){
privateVariable++;
return privateFunction();
};
})();
这个模式与在构造函数中定义特权方法的主要区别,就在于私有变量和函数是由实例共享的。由于 特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个特权方法,作为一个闭包,总是 保存着对包含作用域的引用。来看一看下面的代码。
(function(){
var name
Person = name
};
= "";
function(value){ = value;
Person.prototype.getName = function(){ return name;
};
Person.prototype.setName = function (value){
name = value; };
})();
var person1 = new Person("Nicholas"); alert(person1.getName()); //"Nicholas" person1.setName("Greg"); alert(person1.getName()); //"Greg"
var person2 = new Person("Michael"); alert(person1.getName()); //"Michael" alert(person2.getName()); //"Michael"
以这种方式创建静态私有变量会因为使用原型而增进代码复用,但每个实例都没有自己的私有变 量。到
function
判断变量存在
// 错误
aa == undefined // Uncaught ReferenceError: aa is not defined
// 正确
typeof aa === 'undefined' // true
变量作用域(scope)
变量的作用域是词法作用域
顶层对象
ES5 的顶层对象,本身也是一个问题,因为它在各种实现里面是不统一的。
- 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window。
- 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self。
- Node 里面,顶层对象是global,但其他环境都不支持。
同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。 - ES5之中,顶层对象的属性与全局变量是等价的。
window.a = 1;
a // 1
a = 2;
window.a // 2
var bar = 1;
window.bar //1
var命令和function命令声明的全局变量,依旧是顶层对象的属性;
- es6中let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从ES6开始,全局变量将逐步与顶层对象的属性脱钩。
let b = 1;
window.b // undefined
全局作用域
正常使用的时候禁止this关键字指向全局对象
严格模式和非严格模式
作用域在严格模式和非严格模式下不同
使用’use strict’
严格模式:
‘use strict’
aa = 10 // aa is not defined
给一个没有声明的变量赋值会报错
非严格模式:
aa = 10 // 10
windows.aa // 10
如果给一个没有声明的变量赋值,js 会给全局对象创建一个同名属性
这个 aa 不是全局变量,aa 是全局对象的一个属性
全局变量是不可以删除的
全局对象的属性是可以删除
var globalVar = 10;
globalVar; // => 10
window.globalVar; // => 10
delete globalVar; // => false
// 严格模式下无法删除变量。只有configurable设置为true的对象属性,才能被删除。
globalVar; // => 10
globalArr = 20;
globalArr; // => 20
window.globalArr; // => 20
delete globalArr; // => true
globalArr; // => undefined
添加一个window 已经存在的变量会报错
Uncaught SyntaxError: Identifier ‘parseFloat’ has already been declared
函数作用域
局部作用域就是函数作用域
- 函数参数也是局部变量
var scope = 'global'
function checkscope(scope) {
return scope
}
function checkscope1() {
var scope = 'local'
return scope
}
function checkscope2(scope) {
var scope = 'local1'
return scope
}
console.log(checkscope('local')) // local
console.log(checkscope1()) // local
// 函数体内定义的会把参数的覆盖
console.log(checkscope2('local')) // local1
- 变量提升
- var
var scope = 'global'
function f () {
console.log(scope)
if (true) {
var scope = 'local'
}
console.log(scope)
}
f() // undefined local
// 用 var 定义没有 tdz,所以是 undefined,不是ReferenceError
相当于
var scope = 'global'
function f () {
var scope;
console.log(scope);
if (true) {
scope = 'local'
}
console.log(scope)
}
js 动态执行前会编译下,编译的时候会把 变量的定义提升到函数作用域顶部
因为有变量提升存在所有用 var 定义的变量没有块作用域
- let
{
console.log(foo); // variable.js:2 Uncaught ReferenceError: Cannot access 'foo' before initialization
// foo 已经定义,但是在 tdz 区域,引用错误
let foo = 2;
}
{
console.log(a) // a is not defined
}
用 let 定义的变量没有变量提升,从代码块开始,到变量声明的这段区域是,TDZ,在这一区域访问变量都会报ReferenceError错误。
块作用域 block scope
为了加强对变量生命周期的控制,ECMAScript 6 引入了块级作用域。
块级作用域存在于:
- 函数内部
- 块中(字符 { 和 } 之间的区域)
let 和 const 都是块级声明的一种。
es5
es5的js 没有 block scope,只有 function scope
function test (o) {
var i = 0 // 函数作用域
if (typeof o === 'object') {
var j = 0 // 没有 block scope,是 function scope
for (var k = 0; k < 10; ++k) { // k 也是 function scope
console.log(k)
}
console.log(k)
}
console.log(j)
}
test({})
如果没有块作用域,可以用闭包
var funcs = []
for (var i = 0; i < 10; i++) {
funcs.push(function() {
return i
})
}
funcs.forEach(function(f) {
console.log(f()) // 将在打印10数字10次
})
// i 是全局变量,变量的时候取的都是全局变量 i
// 在JavaScript的闭包中,闭包函数能够访问到包庇函数中的变量,这些闭包函数能够访问到的变量也因此被称为自由变量。只要闭包没有被销毁,那么外部函数将一直在内存中保存着这些变量,在上面的代码中,形参value就是自由变量,return的函数是一个闭包,闭包内部能够访问到自由变量value。同时这儿我们还使用了立即执行函数,立即函数的作用就是在每次迭代的过程中,将i的值作为实参传入立即执行函数,并执行返回一个闭包函数,这个闭包函数保存了外部的自由变量,也就是保存了当次迭代时i的值。
var funcs = []
for (var i = 1; i < 10; i++) {
funcs.push((function(value) {
return function() {
return value
}
})(i))
}
funcs.forEach(function(f) {
console.log(f())
}) // 1...10
es6:
- 限制在块里面
{
let a = 10
}
console.log(a) // a is not defined
- 不允许重复定义
{
let a = 10;
let a = 20; // Uncaught SyntaxError: Identifier 'a' has already been declared
}
- 暂时性死区 tdz
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。
ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
class 和 let 很像
这是因为 JavaScript 引擎在扫描代码发现变量声明时,要么将它们提升到作用域顶部(遇到 var 声明),要么将声明放在 TDZ 中(遇到 let 和 const 声明)。访问 TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会从 TDZ 中移出,然后方可访问。
- let 声明全局变量,不会成为 window 的属性
var a = 10
let b = 10
console.log(window.a) // 10
console.log(window.b) // undefined
循环中的块作用域
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 10
// 解决方案
// 闭包
var a = [];
for (var i = 0; i < 10; i++) {
a[i] = (function () {
return function () {console.log(i);}
})(i);
}
a[6](); // 6
// let
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
for循环用 let定义i 变量为什么可以重复定义?每一次循环的i其实都是一个新的变量
另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。
简单的来说,就是在 for (let i = 0; i < 3; i++) 中,即圆括号之内建立一个隐藏的作用域,然后每次迭代循环时都创建一个新变量,并以之前迭代中同名变量的值将其初始化。
for (var i = 0; i < 3; i++) {
var i = 'abc';
console.log(i);
}
// abc
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
//输出了3次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。
// abc
// abc
// abc
// 伪代码
(let i = 0) {
let i = 'abc';
console.log(i);
}
(let i = 1) {
let i = 'abc';
console.log(i);
}
(let i = 2) {
let i = 'abc';
console.log(i);
};
当执行函数的时候,根据词法作用域就可以找到正确的值,其实你也可以理解为 let 声明模仿了闭包的做法来简化循环过程。
循环中的 let 和 const
var funcs = [];
for (const i = 0; i < 10; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // Uncaught TypeError: Assignment to constant variable.
结果会是报错,因为虽然我们每次都创建了一个新的变量,然而我们却在迭代中尝试修改 const 的值,所以最终会报错。
var funcs = [], object = {a: 1, b: 1, c: 1};
for (var key in object) {
funcs.push(function(){
console.log(key)
});
}
funcs[0]()
结果是 ‘c’;
那如果把 var 改成 let 或者 const 呢?
使用 let,结果自然会是 ‘a’,const 呢? 报错还是 ‘a’?
结果是正确打印 ‘a’,这是因为在 for in 循环中,每次迭代不会修改已有的绑定,而是会创建一个新的绑定。
babel
let value = 1;
编译为:
var value = 1;
if (false) {
let value = 1;
}
console.log(value); // Uncaught ReferenceError: value is not defined
编译为
if (false) {
var _value = 1;
}
console.log(value);
作用
块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。
// IIFE 写法(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
那循环中的 let 声明呢?
var funcs = [];
for (let i = 0; i < 10; i++) {
funcs[i] = function () {
console.log(i);
};
}
funcs[0](); // 0
Babel 巧妙的编译成了:
var funcs = [];
var _loop = function _loop(i) {
funcs[i] = function () {
console.log(i);
};
};
for (var i = 0; i < 10; i++) {
_loop(i);
}
funcs[0](); // 0
let var const
let 和 var 的区别
- let 声明了块作用域,块以外无法访问
- let,const的变量提升标注了暂时性死区的开始,使用let命令声明变量之前,该变量都是不可用的
- let 不允许重复声明
const
通过const生命的变量将会创建一个对该值的一个只读引用,
而且声明的时候要初始化
const MY_FAV = 7
MY_FAY = 20 // 重复赋值将会报错(Uncaught TypeError: Assignment to constant variable)
const foo = {bar: 'zar'}
foo.bar = 'hello world' // 改变对象的属性并不会报错
通过const生命的对象并不是不可变的。但是在很多场景下,比如在函数式编程中,我们希望声明的变量是不可变的,不论其是原始数据类型还是引用数据类型。
const deepFreeze = function(obj) {
Object.freeze(obj)
for (const key in obj) {
if (typeof obj[key] === 'object') deepFreeze(obj[key])
}
return obj
}
const foo = deepFreeze({
a: {b: 'bar'}
})
foo.a.b = 'zar'
console.log(foo.a.b) // bar
最佳实践
但是,当很多开发者开始将自己的项目迁移到ECMAScript2015后,他们发现,最佳实践应该是,尽可能的使用const,在const不能够满足需求的时候才使用let,永远不要使用var。为什么要尽可能的使用const呢?在JavaScript中,很多bug都是因为无意的改变了某值或者对象而导致的,通过尽可能使用const,或者上面的deepFreeze能够很好地规避这些bug的出现,而我的建议是:如果你喜欢函数式编程,永远不改变已经声明的对象,而是生成一个新的对象,那么对于你来说,const就完全够用了。