Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

es6必会之let && const #108

Open
FrankKai opened this issue Oct 7, 2018 · 3 comments
Open

es6必会之let && const #108

FrankKai opened this issue Oct 7, 2018 · 3 comments

Comments

@FrankKai
Copy link
Owner

FrankKai commented Oct 7, 2018

一直以来都有用let和const,看似深入学习过,但其实没有真正完全理解,在模棱两可的用,之前在城西的一次面试就被问的哑口无言,所以我将通过mdn的资料针对性地学习letconst,并且会写一些易懂的例子,也会涉及到一些规范里的内容。

  • block,statement和expression的区别是什么?
  • 为什么选择了'let'作为block-scoped variable declaration?
  • let和const不会像var一样绑定值到global 对象!
  • let和const不能像var一样同一个scope下声明多次!
  • let和const不会像var一样变量声明提升!
  • 循环中let/var与setTimeout的羁绊!
    • for循环无异步参与时,var,let效果相同
    • for循环有异步参与时,var异常,let创建块作用域
    • Array.prototype.forEach与setTimeout,独立作用域
@FrankKai
Copy link
Owner Author

FrankKai commented Oct 7, 2018

let

let声明了一个block scope local variable,选择性为其赋值。

let x = 1;
if(x === 1) {
    let x = 2;
    console.log(x); // 2
}
console.log(x); // 1

let使变量的作用域限制于block,statement,或者expression。

block,statement和expression的区别是什么?

这对于作用域的判别很有用。

  • block curly bracket,circle bracket等等。
  • statement js由各种语句组成,Control Flow,Function,Iterations等等。
  • expression 副作用,无副作用;可执行和关键词。

很多东西自以为会了,然而实际上可能只是走马观花,所以回过头来将基础捡起来,可能比学习高阶技术更重要。

block
  • group zero or more statements
  • block 由 curly bracket包裹(弯曲的支架比花括号更形象)
  • blcok statement 在其他语言里通常叫做computed statement,之所以叫复合语句,因为JavaScript会一句一句执行,在block 里可以有多个statement,
var x = 1;
let y = 1;
if (true) {
  var x = 2;
  let y = 2;
}
console.log(x);
console.log(y);

这里的block指的是{var x = 2; let y = 2;},注意:不包括if(true),因为它位于curly bracket之外。
Block Statement Syntax:

{
    StatementList
}

比起block statement syntax,更重要的是Block Scoping Rules
1.var rule:
一种是全局定义,一种是函数局部定义。局部函数变量的访问会从作用域链由下往上找。但是这种老式的变量命名符不提倡再使用。

var x = 1;
{
    var x = 2;
}
console.log(x); // 2

2.let && const rule:

let x = 1;
{
    x = 2;
}
console.log(x); // 1
const x = 1;
{
   x = 2;
}
console.log(x); // 1

由于block scoping的存在,不会throw出Uncaught SyntaxError: Identifier 'x' has already been declared
3.function rule:

foo('outside'); // TypeError: foo is not a function
{
    function foo(location) {
        console.log('foo is called' + location);
    }
    foo('inside'); // foo is called inside
}

更准确一些的说法是,block statement阻止function declaration 被hoisted(变量提升)到作用域的顶部。这种定义方式与函数表达式类似。
函数表达式的变量提升规则是下面这样:

foo('before'); // Uncaught TypeError: foo is not a function
var foo = function(location) {
    console.log('foo is called' + location);
}
foo('after'); // foo is called after

function块作用域规则同上:

foo('before'); // TypeError: foo is not a function
{
    function foo(location) {
        console.log('foo is called' + location);
    }
}
foo('after'); // foo is called after

与函数表达式不会提升到var foo = function(){}一样;{}内部定义的function,不会提升到{}之前。而这正是function的blocking statement rule。

statement
什么是empty statement?
var array = [1, 2, 3];
for (i=0; i<array.length; array[i++] = 0) /* empty statement */;
console.log(array);
  • Javascript 应用就是由符合语法的多个statement组成的。
  • 一条statement可能跨越多行。
  • 多条statement也可能在一行中出现,每一句由一个分号标记。
  • statement可以分为Control flow,Declarations,Functions and classed,Iterations,Others

Control flow包括Block,break,continue,Empty,if...else,switch,throw,try...catch
Declarations包括var,let,const
Functions and classed包括function,function *,async function,return,class
Iterations包括:do...while,for,for each...in,for...in,for...of,while
Others包括:debugger,export,import,import.meta,label,with

什么是Expressions?
  • expression指的是任何可以解析为value的代码单元。
  • 2种有效的expression:有side effect的,例如x = 7;某些情况下执行并且解析为值,例如3 + 4。
  • 还有2种分类,一类是执行后为number,string,boolean型的;一类是关键词类型,这种类型又分为Primary expression和Left-hand-side expressions。

Primary expressions
Basic keywords and general expressions in JavaScript,例如this,grouping operator.

  • this
    当前对象的引用,2种调用对象方法的方式this['propertyName'],this.propertyName
function validate(obj, lowval, hival) {
  if ((obj.value < lowval) || (obj.value > hival))
    console.log('Invalid Value!');
}
<p>Enter a number between 18 and 99:</p>
<input type="text" name="age" size=3 onChange="validate(this, 18, 99);">

上面的例子中,this指代input这个DOM对象,它由于具有属性value,因此可以调用validate函数,并且每次输入值发生变化时都触发onChange回调。

  • Grouping operator
    ()
var a = 1;
var b = 2;
var c = 3;
// default precedence
a + b * c     // 7
// evaluated by default like this
(a + b) * c   // 9

Left-hand-side expressions
左值是赋值的目标,例如new,super,Spread operator
new 创建一个用户自定义object类型

var objectName = new objectType([param1, param2, ..., paramN]);

super 调用当前object的父object上的函数,在class中常用。

super([arguments]); // 调用parent constructor
super.functionOnParent([arguments]); // 调用parent上的方法

Spread operator 允许表达式被展开,可以是函数参数处展开,也可以是数组迭代处展开。
数组某处插入数组元素。

var parts = ['shoulders', 'knees'];
var lyrics = ['head', ...parts, 'and', 'toes'];

一个完整数组作为参数传入函数

function f(x,y,z){}
var args = [0,1,2];
f(...args);

通过对block,statement,expression的回顾。我们发现,其实块作用域不仅仅是curly bracket,{}。在for(){}for(key in object)for(item of array)等等的()内,其实也属于块作用域,不仅仅是if else的{},for{}中的{}才算是块作用域,let都有效。

let a = 1;
for(let a = 2; a<3; a++){
    console.log(a);
};
console.log(a);
// 2 1
let key = 'hello world';
for(let key in {foo:1,bar:2}){
    console.log(key);
}
console.log(key);
// foo bar hello world

若是不用let,会将全局的key override,所以在for系列的循环控制语句中使用let很有必要。

let key = 'hello world';
for(key in {foo:1,bar:2}){
    console.log(key);
}
console.log(key);
// foo bar bar

for(item of array)中也一样。

let item = 4;
for(let item of [1,2,3]){
	console.log(item);
}
console.log(item);
// 1 2 3 4
let item = 4;
for(item of [1,2,3]){
	console.log(item);
}
console.log(item);
// 1 2 3 3

使用let以后,井水不犯河水,不用担心改写全局中的同名变量,但是一定要明确,let不仅仅作用于{},()也同样作用。

为什么选择了'let'作为block-scoped variable declaration?

可以看这个stack overflow上的question:Why was the name 'let' chosen for block-scoped variable declarations in JavaScript?
有两点比较重要:

  1. 参考了scala,F#等语言里比variable用作更高级抽象的let;
  2. 一个很有趣的解释:let myPet = 'dog', let my pet be a dog。

let和const不会像var一样绑定值到global 对象!

众所周知,var会绑定变量到global对象(不一定是window,global,还可能是Vue instance),但是let和const不会。

var foo = 1;
let bar = 2;
const baz = 3;
console.log(this.foo, this.bar, this.baz); //1 undefined undefined 

let和const不能像var一样同一个scope下声明多次!

let foo = 1;
let foo = 2; // Uncaught SyntaxError: Identifier 'foo' has already been declared
const foo = 1;
const foo = 2; // Uncaught SyntaxError: Identifier 'foo' has already been declared
var foo = 1;
var foo = 2; // everything is ok

let和const不会像var一样变量声明提升!

原因是:const,let存在temporal dead zone!

因此不能let ,const赋值前使用变量。

在说变量提升之前,先了解一个概念,Temporal Dead Zone,指的是从block创建到初始化完成之间的时间。用var不会存在Temporal Dead Zone,因为用var声明的变量,初始值立即默认赋予undefined,不会像let这样,存在Temporal Dead Zone,不会立即为其赋undefined,所以会报ReferenceError错误。

function do_something() {
    console.log(bar); // undefined
    console.log(foo); // ReferenceRrror
    var bar = 1;
    let foo = 2;
}

正是由于let存在temporal dead zone,没有立即为变量赋初始值为undefined,所以typeof的结果为ReferenceRrror。

console.log(typeof undeclaredVariable); // undefined
console.log(typeof i);// ReferenceError,存在temporal dead zone
let i = 10;

var,let,const都会变量提升,但是仅仅是对var foo;let foo;const foo的提升,而不是var foo = 1;let foo =1;const foo = 1;整体提升!

let不会立即为变量赋undefined初值是好是坏呢?当然是好事!这样将变量的管理更加精细,避免引用重名变量覆盖后出现bug还发现不了的情况。

还有两个temporal dead zone的情况:

function test(){
   var foo = 33;
   if (true) {
      let foo = (foo + 55); // ReferenceError:Cannot access 'foo' before initialization
   }
}
test();

(foo + 55)中的foo在let foo初始化(正在赋值)的过程中访问,也就是在temporal dead zone期间进行访问,因此会抛出引用异常

function go(n) {
  console.log(n);
  for (let n of n.a) { // ReferenceError,Cannot access 'n' before initialization
    console.log(n);
  }
}
go({a: [1, 2, 3]});

n.a中的n在let n初始化(正在赋值)的过程中访问,也就是在temporal dead zone期间进行访问,因此会抛出引用异常

@FrankKai
Copy link
Owner Author

FrankKai commented Oct 7, 2018

const

其实在let模块已经写了很多关于const的内容,所以在这里就写一些const特有的特性。

  • const也是block-scoped的,和用let定义的变量类似。
  • 不可以修改变量值,也就是不可以reassignment,并不是immutable
  • 不可以重新定义
  • const foo = [value],value可以是function,而let也可以!
  • 必须为const赋一个初值且存在temporal dead zone,比let更加严格!
const foo = 1;
{
    const foo =2;
}
const foo = 1;
foo = 2; // Uncaught TypeError: Assignment to constant variable.
const foo = 1;
const foo = 2; // Uncaught SyntaxError: Identifier 'foo' has already been declared

let定义的变量赋值function会有什么错误提示呢?

let foo = function(){
    console.log('foo');
}
foo();// foo

不会报错,但是因为let可以reassignment,所以不如const更加安全,因为一般来说,我们创建一个函数以后,不太会再去覆盖这个函数。

const不可以reassignment,并不是immutable什么意思?

immutable指的是变量的值完全不可改变,例如'hi',{foo:1,bar:2},若这个字符串和对象是immutable的,那么'hi'完全不能被修改,而且对象{foo:1,bar:2}也完全不能修改,也就是说它的属性foo和bar值都不能修改,但是const只是约束了reassignment,没有约束mutable。

下面这种写法是完全OK的:

const obj = {
    foo: 1,
    bar: 2,
}
obj.foo = 3;
console.log(obj); // {foo: 3,bar:2}

cosnt不赋初值有什么报错?

cosnt foo;// Uncaught SyntaxError: Missing initializer in const declaration

假设修改了原型链上的属性会怎样?

const foo = 'foo';
foo.length = 5; // return 5
console.log(foo.length); // 3

我们可以看出,const还是很包容的,即使你试图修改原型链上的属性,也不会报错,他只是一笑而过,并且这种修改不会生效。

const真的很严格!

类型 是否必须赋值 是否存在temporal dead zone 是否支持redeclaration 是否支持reassignment
var
let
const

2018年12月18日01:45更新

es6的const和java的final之间的对比

关于const不支持reassignment这一点,java中的关键字final与之对应,表示这个常量只能赋值一次。
但是java多了一个类常量的概念,所谓类常量,指的其实就是一个常量在类的多个方法中有调用, 也就是static final foo = "foo";这样的形式。
在js中就没有这种讲究了,const就是代表常量,我才不管你是不是类的还是实例的。


2020年6月4日更新

在定义前引用的话,let和const会报什么错?

foo();
let foo = () =>{console.log('foo')};
// const foo = () =>{console.log('foo')};

let: ReferenceError: xxx is not defined
const : ReferenceError: Cannot access 'xxx' before initialization

@FrankKai
Copy link
Owner Author

FrankKai commented Oct 7, 2018

for循环中let/var与setTimeout的羁绊!

无异步参与时,var,let效果相同

for (var i = 0; i<9 ;i++){
    console.log(i); // 0, 1, 2, 3, 4, 5, 6, 7, 8
}
for (let i = 0; i<9 ;i++){
    console.log(i); // 0, 1, 2, 3, 4, 5, 6, 7, 8
}

有异步参与时,var异常,let创建块作用域

for (var i = 0; i<9 ;i++){
    setTimeout(()=>{
        console.log(i);
    })
}

输出了9次9。
这是因为var声明的i是全局变量,只声明了一次,当9次循环结束时,此时的i值为8,再次执行一次i++,i变为9,此时9个定时器开始打印,打印出的结果是全局的值为9为i变量。

  1. i从0加到8,创建了9个定时器
  2. i++,全局变量i变为9
  3. 9个定时器执行,打印9次全局变量9
for (let i = 0; i<9 ;i++){
    setTimeout(()=>{
        console.log(i);
    })
}

依次从0到8输出。
这是因为let声明的i变量,会声明9次,当9次循环结束时,创建出9个块作用域,此时的i值为8,再执行一次++,i变为9,但是此时的i(9)与之前创建出的 setTimeout(()=>{console.log(0);}) setTimeout(()=>{console.log(1);}) setTimeout(()=>{console.log(2);}) setTimeout(()=>{console.log(3);}) setTimeout(()=>{console.log(4);}) setTimeout(()=>{console.log(5);}) setTimeout(()=>{console.log(6);}) setTimeout(()=>{console.log(7);}) setTimeout(()=>{console.log(8);})中的i(0),i(1),i(2),i(3),i(4),i(5),i(6),i(7),i(8)是完全不同的i,setTimeout中的i是独立的块作用域中的i。

用I don't know js中的一段话来讲就是:>let为每次迭代声明一次变量,后续迭代的初始值是上一次迭代的结束值。

Array.prototype.forEach与setTimeout

['foo','bar','baz'].forEach((_, i)=>{
    setTimeout(()=>{console.log(i)})
})

依次输出0,1,2。
这是因为Array.prototype.forEach迭代器中的i,是传入的callback回调函数返回的,与let类似,会创建出3个独立的块所用域, setTimeout(()=>{console.log(0);}) setTimeout(()=>{console.log(1);}) setTimeout(()=>{console.log(2);})

所以使用Array.prototype.forEach时根本无需担心setTimeout的问题,没有let/var与setTimeout的困扰,在实际开发中是比较常见的场景。

注意:

  1. setTimeout(()=>{console.log(i)},i)中的延时参数i,var时是全局,let是块作用域
  2. 无论循环多少次,i从几开始,setTimeout延时参数i怎么变化,关键区分var与let即可解决所有问题

关键词:

letconstblocking scopetemporal dead zoneredeclarationreassignmentimmutableinitializer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant