js基石系列大纲
变量
变量 是存储值的容器,变量就是为“值”起名,然后引用这个名字,就等同于引用这个值。变量的名字就是变量名。
变量的声明
var
作用是声明一个变量,按照早期JavaScript的约定,在全局代码块使用var声明时,相当于在window上声明了一个属性,进而使所有代码都能将这些声明作为全局变量来访问。
var names = 'jimmy'
console.log(window) // 打印window,变量已经挂载上去
也可以通过省略var来声明变量,这时变量会变成全局变量,挂载到window下,比如在函数里用此方式声明一个变量,函数外层也可以访问到。这种做法是不推荐或者说禁止的,因为在局部作用域中不用var等声明变量很难维护,也会造成困惑,不利于表达意图,作用域也变得更加宽泛,不能一下子断定省略var是不是有意而为之。在严格模式下,如果像这样给未声明的变量赋值,则会导致抛出 ReferenceError
function test() {
names = "jimmy"; // 全局变量
}
test();
console.log(names); // "jimmy"
目前实际开发中,定义变量已经不在使用var
let/const
let,const 跟 var 的作用都是声明变量,但它们有着非常重要的区别。最明显的区别是 块级作用域,下面我们来详细看看它们的区别:
var,let,const三者的区别:
- 块级作用域
块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。
- 变量提升
var存在变量提升,let和const不存在变量提升.
- 给全局添加属性
浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
- 重复声明
var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
暂时性死区
在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
- 初始值设置
在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
- 指针指向
let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。
注意
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值等),值就保存在变量指向的那个内存地址。但对于引用类型的数据(主要是对象和数组),变量指向的是内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的,至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。
const person = {};
// 为 person 添加一个属性,可以成功
person.age = 123;
// 将 person 指向另一个对象,就会报错
person = {}; // TypeError: "person" is read-only
块级作用域
块级作用域由最近的一对包含花括号{} 界定。换句话说, if 块、while 块、function块,单独的块块级作用域。
let和const实际上为 JavaScript 新增了块级作用域。
function f1() {
let n = 5;
if (true) {
let n = 10;
}
console.log(n); // 5
}
上面的函数有两个代码块,都声明了变量n,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用var定义变量n,最后输出的值是 10。
块级作用域的出现,实际上使得获得广泛应用的匿名立即执行函数表达式(匿名 IIFE)不再必要了。
// IIFE 写法
(function () {
var tmp = ...;
...
}());
// 块级作用域写法
{
let tmp = ...;
...
}
变量提升
前面我们讲了,var与let,const的区别之一是变量提升,那我们来具体了解一下变量提升。
什么是变量提升?
先来看看MDN中对变量提升的描述:
变量提升(Hoisting)被认为是,Javascript 中执行上下文(特别是创建和执行阶段)工作方式的一种认识。在 ECMAScript® 2015 Language Specification 之前的 JavaScript 文档中找不到变量提升(Hoisting)这个词。
从概念的字面意义上说,“变量提升”意味着变量和函数的声明会在物理层面移动到代码的最前面,但这么说并不准确。实际上变量和函数声明在代码里的位置是不会动的,而是在编译阶段被放入内存中。
通俗来说,变量提升是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的行为。变量被提升后,会给变量设置默认值为 undefined。 正是由于 JavaScript 存在变量提升这种特性,导致了很多与直觉不太相符的代码,这也是 JavaScript 的一个设计缺陷。虽然 ECMAScript6 已经通过引入块级作用域并配合使用 let、const 关键字,避开了这种设计缺陷,但是由于 JavaScript 需要向下兼容,所以变量提升在很长时间内还会继续存在。
在 ECMAScript6 之前,JS 引擎用 var 关键字声明变量。在 var 时代,不管变量声明是写在哪里,最后都会被提到作用域的顶端。 下面在全局作用域中声明一个num 变量,并在声明之前打印它:
console.log(num)
var num = 1
这里会输出 undefined,因为变量的声明被提升了,它等价于:
var num
console.log(num)
num = 1
可以看到,num 作为全局变量会被提升到全局作用域的顶端。
除此之外,在函数作用域中也存在变量提升:
function getNum() {
console.log(num)
var num = 1
}
getNum()
这里也会输出 undefined,因为函数内部的变量声明会被提升至函数作用域的顶端。它等价于:
function getNum() {
var num
console.log(num)
num = 1
}
getNum()
除了变量提升,函数实际上也是存在提升的。JavaScript中函数的声明形式有两种:
//函数声明式:
function foo () {}
//变量形式声明:
var fn = function () {}
当使用变量形式声明函数时,和普通的变量一样会存在提升的现象,而函数声明式函数会提升到作用域最前边,并且将声明内容一起提升到最上边。如下:
fn()
var fn = function () {
console.log(1)
}
// 输出结果:Uncaught TypeError: fn is not a function
foo()
function foo () {
console.log(2)
}
// 输出结果:2
可以看到,使用变量形式声明fn并在其前面执行时,会报错fn不是一个函数,因为此时fn只是一个变量,还没有赋值为一个函数,所以是不能执行fn方法的。
变量提升导致的问题
由于变量提升的存在,使用 JavaScript 来编写和其他语言相同逻辑的代码,都有可能会导致不一样的执行结果。主要有以下两种情况。
(1)变量被覆盖
来看下面的代码:
var name = "JavaScript"
function showName(){
console.log(name);
if(0){
var name = "CSS"
}
}
showName()
这里会输出 undefined,而并没有输出“JavaScript”,为什么呢?
首先,当刚执行 showName 函数调用时,会创建 showName 函数的执行上下文。之后,JavaScript 引擎便开始执行 showName 函数内部的代码。首先执行的是:
console.log(name);
执行这段代码需要使用变量 name,代码中有两个 name 变量:一个在全局执行上下文中,其值是JavaScript;另外一个在 showName 函数的执行上下文中,在函数执行过程中,JavaScript 会优先从当前的执行上下文中查找变量,由于变量提升的存在,当前的执行上下文的头部就声明了name,其值是 undefined,所以获取到的 name 的值就是 undefined。
这里输出的结果和其他支持块级作用域的语言不太一样,比如 C 语言输出的就是全局变量,所以这里会很容易造成误解。
(2)变量没有被销毁
function foo(){
for (var i = 0; i < 5; i++) {
// Todo
}
console.log(i); // 5
}
foo()
使用其他的大部分语言实现类似代码时,在 for 循环结束之后,i 就已经被销毁了,但是在 JavaScript 代码中,i 的值并未被销毁,所以最后打印出来的是 5。这也是由变量提升而导致的,在创建执行上下文阶段,变量 i 就已经被提升了,所以当 for 循环结束之后,变量 i 被console.log使用了,并没有被销毁。
暂时性死区
我们再来看看暂时性死区的概念:
只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError
let tmp;
}
上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。
ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。
if (true) {
// TDZ开始
tmp = 'abc'; // ReferenceError
console.log(tmp); // ReferenceError
let tmp; // TDZ结束
console.log(tmp); // undefined
tmp = 123;
console.log(tmp); // 123
}
上面代码中,在let命令声明变量tmp之前,都属于变量tmp的“死区”。
“暂时性死区”也意味着typeof不再是一个百分之百安全的操作。
typeof x; // ReferenceError
let x;
上面代码中,变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError。
作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。
typeof undeclared_variable // "undefined"
上面代码中,undeclared_variable是一个不存在的变量名,结果返回“undefined”。所以,在没有let之前,typeof运算符是百分之百安全的,永远不会报错。现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。
有些“死区”比较隐蔽,不太容易发现。
function bar(x = y, y = 2) {
return [x, y];
}
bar(); // 报错
上面代码中,调用bar函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于“死区”。如果y的默认值是x,就不会报错,因为此时x已经声明了。
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
另外,下面的代码也会报错,与var的行为不同。
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
上面代码报错,也是因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“。
ES6 规定暂时性死区和let、const语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
思考
var 为撒会被淘汰?
结合上面的知识,笔者认为var被淘汰的原因有以下几个方面
- 局部变量挂载到全局对象,造成全局对象的污染
- 允许重复变量的声明,导致数据被覆盖
- 无法声明常量,没有块级作用域
- 变量提升
任何一种技术或者新语法的出现,大都是为了解决现有的痛点或者问题,所以ES6中let和const的出现原因以及作用,解决了什么问题,我想大家现在应该都有了自己的答案!!