let

众所周知,在ES6之前,声明变量的关键字就只有var。var 声明变量要么是全局的,要么是函数级的,而无法是块级的。

var a=1;
console.log(a); //
console.log(window.a); //

function test(){
  var b=2;
  function print(){
    console.log(a,b);
  }
print();
}
test(); //1 2
console.log(b); //Uncaught ReferenceError: b is not defined

for(var i=0;i<=10;i++){
var sum=0;
sum+=i;
}
console.log(i); //
console.log(sum); //10 声明在for循环内部的i和sum,跳出for循环一样可以使用。

再来看看下面这个栗子:

HTML:
<ul>
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
JS:
window.onload = function(){
var aLi = document.getElementsByTagName('li');
for (var i=0;i<aLi.length;i++){
aLi[i].onclick = function(){
alert(i);
};
}

这是一道很经典的笔试题,也是很多初学者经常犯错而且找不到原因的一段代码。想要实现的效果是点击不同的<li>标签,alert出其对应的索引值,但是实际上代码运行之后,我们会发现不管点击哪一个<li>标签,alert出的i都为4。因为在执行for循环之后,i的值已经变成了4,等到点击<li>标签时,alert的i值是4。在ES6之前,大部分人会选择使用闭包来解决这个问题,今天我们使用ES6提供的let来解决这个问题。接下来就看看let的神奇吧。

window.onload = function(){
var aLi = document.getElementsByTagName('li');
for (let i=0;i<aLi.length;i++){
aLi[i].onclick = function(){
alert(i);
}
};
}

有看出什么区别吗?奥秘就在for循环中var i=0变成了let i=0,我们仅仅只改了一个关键字就解决了这个问题,还避免了使用闭包可能造成的内存泄漏等问题。

上述代码中的for 循环头部的 let 不仅将 i 绑定到了 for 循环的块中, 事实上它将其重新绑定到了循环的每一个迭代中, 确保使用上一个循环迭代结束时的值重新进行赋值。

后面就让我们好好来了解一下let这个神奇的关键字吧。

let 关键字可以将变量绑定到所在的任意作用域中(通常是 { .. } 内部)。换句话说,let为其声明的变量隐式地了所在的块作用域。  ----《你所不知道的JavaScript(上)》P32

上述代码,可以通过另一种方式来说明每次迭代时进行重新绑定的行为:

window.onload = function(){
var aLi = document.getElementsByTagName('li');
for (let i=0;i<aLi.length;i++){
let j = i;
aLi[j].onclick = function(){
alert(j);
}
};
}

在这里还有个点要说明的,就是 for循环还有一个特别之处,就是循环语句部分是一个父作用域,而循环体内部是一个单独的子作用域。

这就很好理解上面这段代码的意思了。每次循环体执行的时候,let声明的变量 j 会从父作用域(循环语句块)取值保存到自己的块级作用域内,由于块级作用域内的变量不受外部干扰,所以每次循环体生成的块级作用域相互独立,各自保存着各自的 j 值。


来看一下 let 和 var 的一些异同吧。

1、函数作用域 vs 块级作用域

function varTest() {
var x = 31;
if (true) {
var x = 71; // same variable!
console.log(x); //
}
console.log(x); //
} function letTest() {
let x = 31;
if (true) {
let x = 71; // different variable
console.log(x); //
}
console.log(x); //
}

可以看出在letTest函数的 if 判断中重新声明的x并不会影响到 if 代码块之外的代码,而varTest函数中用var声明的却会。这是因为let声明的变量只在代码块(通常是{ }所形成的代码块)中有效。

2、变量提升 vs 暂时性死区

我们都知道,var声明的变量会有变量提升的作用,如下

console.log(a);  //
var a=1; console.log(b); //undefined
var b;

可以看出,虽然代码中console调用a在前,声明a在后,但是由于在js中,函数及变量的声明都将被提升到函数的最顶部,也就是说(var声明的)变量可以先使用再声明。

然后,使用let,const(后面会提及)声明的变量却不存在变量提升。

console.log(foo); // Uncaught ReferenceError: foo is not defined
let foo = 2; console.log(foo1); // Uncaught ReferenceError: foo1 is not defined
let foo1;

ES6明确规定,如果区块中存在let命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。所以在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

注:“暂时性死区”也意味着typeof不再是一个百分之百安全的操作,因为会使typeof报错。

3、let不允许重复声明

if (true) {
let aa;
let aa; // Uncaught SyntaxError: Identifier 'aa' has already been declared
} if (true) {
var _aa;
let _aa; // Uncaught SyntaxError: Identifier '_aa' has already been declared
} if (true) {
let aa_;
var aa_; // Uncaught SyntaxError: Identifier 'aa_' has already been declared
}

let不允许在相同作用域内,重复声明同一个变量。

4、全局变量 vs 全局对象的属性

ES5中全局对象的属性与全局变量基本是等价的,但是也有区别,比如通过var声明的全局变量不能使用delete从 window/global ( global是针对与node环境)上删除,不过在变量的访问上基本等价。

ES6 中做了严格的区分,使用 var 和 function 声明的全局变量依旧作为全局对象的属性,使用 letconst 命令声明的全局变量不属于全局对象的属性。

let let_test = 'test';
console.log(window.let_test); // undefined
console.log(this.let_test); // undefined var var_test = 'test';
console.log(window.var_test); // test
console.log(this.var_test); // test

const

除了let以外,ES6还引入了const,同样可以用来创建块作用域变量,但其值是固定的(常量)。使用const声明变量的时候,必须同时赋值,否则会报错。并且之后任何试图修改值的操作都会引起错误.

const data;  //Uncaught SyntaxError: Missing initializer in const declaration
if (true) {
var a = 2;
const b = 3; // 包含在 if 中的块作用域常量
a = 3; // 正常 !
b = 4; // Uncaught TypeError: Assignment to constant variable.
}
console.log( a ); //
console.log( b ); // Uncaught ReferenceError: b is not defined

注:复合类型const变量保存的是引用。因为复合类型的常量不指向数据,而是指向数据(heap)所在的地址(stack),所以通过 const 声明的复合类型只能保证其地址引用不变,但不能保证其数据不变。

const arr= [1, 2];

// 修改数据而不修改引用地址,正确执行
arr.push(3); // [1, 2, 3] // 修改 arr 常量所保存的地址的值,报错
arr = []; // Uncaught TypeError: Assignment to constant variable.

简单的使用const无法完成对象的冻结。可以通过Object.freeze()方法实现对对象的冻结。使用Object.freeze()方法返回的对象将不能对其属性进行配置(definedProperty()不可用)同时不能添加新的属性和移除(remove)已有属性。彻底冻结对象时需要递归的对它的对象属性进行冻结。

let obj = {
a: 1,
b: {
b1: 2
}
}; obj.b.b1 = 3;
console.log(obj.b.b1 ); // function freeze(obj){
Object.freeze(obj);
Object.values(obj).forEach(function (value,index) {
if(typeof value === 'object'){
freeze(value);
}
})
} freeze(obj); obj.b.b1 = 4;
console.log(obj.b.b1); //

总结:

块级作用域的出现,让广泛使用的 IIFE (立即执行匿名函数)不再必要。

// 匿名函数写法
(function () {
var jQuery = function() {};
// ...
window.$ = jQuery
})(); // 块级作用域写法
{
let jQuery = function() {};
// ...
window.$ = jQuery;
}

附:在ES6之前,关键字with和关键字try/catch都会创建相关的块级作用域。关键字with已经不推荐使用了,我们在这里就不多描述。在ES3规范中规定try/catch的catch分句会创建一个块作用域,其中声明的变量仅在catch内部有效。

try {
undefined(); // 执行一个非法操作来强制制造一个异常
}
catch (err) {
console.log( err ); // 能够正常执行!
}
console.log( err ); // Uncaught ReferenceError: err is not defined

let和const----你所不知道的JavaScript系列(2)的更多相关文章

  1. js值----你所不知道的JavaScript系列(6)

    1.数组 在 JavaScript 中,数组可以容纳任何类型的值,可以是字符串.数字.对象(object),甚至是其他数组(多维数组就是通过这种方式来实现的) .----<你所不知道的JavaS ...

  2. js类型----你所不知道的JavaScript系列(5)

    ECMAScirpt 变量有两种不同的数据类型:基本类型,引用类型.也有其他的叫法,比如原始类型和对象类型等. 1.内置类型 JavaScript 有七种内置类型: • 空值(null) • 未定义( ...

  3. 闭包----你所不知道的JavaScript系列(4)

    一.闭包是什么? · 闭包就是可以使得函数外部的对象能够获取函数内部的信息. · 闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分. · 闭包就 ...

  4. 提升----你所不知道的JavaScript系列(3)

    很多编程语言在执行的时候都是自上而下执行,但实际上这种想法在JavaScript中并不完全正确, 有一种特殊情况会导致这个假设是错误的.来看看下面的代码, a = 2; var a; console. ...

  5. LHS 和 RHS----你所不知道的JavaScript系列(1)

      变量的赋值操作会执行两个动作, 首先编译器会在当前作用域中声明一个变量(如果之前没有声明过), 然后在运行时引擎会在作用域中查找该变量, 如果能够找到就会对它赋值.----<你所不知道的Ja ...

  6. 你所不知道的JavaScript数组

    相信每一个 javascript 学习者,都会去了解 JS 的各种基本数据类型,数组就是数据的组合,这是一个很基本也十分简单的概念,他的内容没多少,学好它也不是件难事情.但是本文着重要介绍的并不是我们 ...

  7. 你所不知道的javascript数组特性

    工作中,我们经常使用js的数组,但是,下面的东西你见过吗? 1,文本下标: var a=[]; a[-1]=1; 你想过数组的下标为负数的情况吗?我们对数组的下标规定从0开始.但是上面那么写也还是可以 ...

  8. JavaScript中你所不知道的Object(二)--Function篇

    上一篇(JavaScript中你所不知道的Object(一))说到,Object对象有大量的内部属性,而其中多数和外部属性的操作有关.最后留了个悬念,就是Boolean.Date.Number.Str ...

  9. 一些你所不知道的VS Code插件

    摘要: 你所不知道的系列. 原文:提高 JavaScript 开发效率的高级 VSCode 扩展之二! 作者:前端小智 Fundebug经授权转载,版权归原作者所有. 作为一名业余爱好者.专业人员,甚 ...

随机推荐

  1. php编译安装 报错 make ***[libphp5.la] Error 1解决方法

    报错信息: /usr/bin/ld: cannot find -lltdl collect2: ld returned 1 exit status make: *** [libphp5.la] Err ...

  2. IE9 添加事件DOMContentLoaded,方法addEventListener

    IE9 新添加 事件DOMContentLoaded,方法addEventListener

  3. 【优质】React的学习资源

    React的学习资源 github 地址: https://github.com/LeuisKen/react-collection https://github.com/reactnativecn/ ...

  4. [翻译] 提升树算法的介绍(Introduction to Boosted Trees)

    [翻译] 提升树算法的介绍(Introduction to Boosted Trees) 1. 有监督学习的要素 XGBoost 适用于有监督学习问题.在此类问题中,我们使用多特征的训练数据集 \(x ...

  5. 阿里八八Alpha阶段Scrum(12/12)

    今日进度 俞鋆: 和前端对接,完成了注册接口的对接写了一部分数据同步接口. 李嘉群: 完成了注册界面与服务器的对接 黄梅玲: json数据解析并在listview中显示(还未完成) 王国超: 今天开始 ...

  6. EF CodeFirst 数据库的操作

    生成数据库 首先需要通过Nuget安装Migration 这一步是初始化Migration操作. 首先打开程序包控制台, 工具——NuGet包管理器——程序包管理控制台 打开后,在控制台输入下面的命令 ...

  7. SGU刷题之路开启

    VJ小组:SGU---48h/题 每道做出的题目写成题解,将传送门更新在这里. SGU:101 - 200 SGU - 107 水题 解题报告 SGU - 105 找规律水题 解题报告 SGU - 1 ...

  8. 1.HBase In Action 第一章-HBase简介(后续翻译中)

    This chapter covers ■ The origins of Hadoop, HBase, and NoSQL ■ Common use cases for HBase ■ A basic ...

  9. PHP判断用户是否手机访问

    自定义的函数如下: $agent = check_wap(); if( $agent ) { header('Location: http://www.nowamagic.net'); exit; } ...

  10. 离线安装Cloudera Manager 5和CDH5(最新版5.9.3) 完全教程(一)环境说明

    关于CDH和Cloudera Manager CDH (Cloudera's Distribution, including Apache Hadoop),是Hadoop众多分支中的一种,由Cloud ...