✍️ Tangxt | ⏳ 2020-05-25 | 🏷️ ES6 |
基于前边所讲的内容,还有一些特殊的情况需要处理,如面对用var和直接function声明一个变量的方式,会有提前声明的特殊性。
当然,就平时开发来说,我们很少会用var
了,而 function xxx() {}
倒是经常在用
所以我们一般都不去考虑 var
声明的变量会有变量提升这件事儿……
既然你var特殊,那我就不用你,这样我就不用去考虑你的特殊性了
有这样一段代码:
console.log(a) //undefined
var a = 12
console.log(a) //12
之前我们说到,要执行这代码会走怎么一个流程:
在当前EC代码执行之前:
var
或者function
关键的声明或定义给提上来(注意:带var的只是提前声明,而带function的则会提前的声明+定义)因此,上边代码的执行流程是这样的:
var a
提前声明了,但未定义,即为undefined
值console.log(a)
-> 看到VO(G)里的a是undefined值a = 12
-> 改变VO(G)里的a的状态为12
console.log(a)
-> 12那function
呢?-> 它更有特殊性
如:
console.log(a)
fn()
var a = 12
function fn() {
console.log('hi')
}
执行过程:
fn[[scope]] = VO(G)
console.log(a)
fn()
自打ES6出现后,我们都不怎么管变量提升的事儿了……
以下只是测试,平时不会写出现这样的代码:
注意,同一个EC上,出现相同的变量,并不会重新声明,但会重新赋值
上图的另一种理解姿势(老师认为的是这种,嗯,我也认同):
var a
写在 function a
上边,那么就 var a
先提前声明,而之后的 function a
并不是声明而是定义 a
的值。换个位置来看,function a
先声明a,之后,var a
忽视掉,因为不会重新声明,也不会所谓的给个 undefined
值替换掉这种理解姿势才是有道理的,不然浏览器难道先把所有代码都看一遍,即先把
var
声明的变量给拎出来,然后再把function声明的变量给拎出来? -> 还是看一个声明一个比较有道理,遇到已经声明过的,就跳过,但function除了声明还有定义,所以就得重新定义一下声明过的变量
不管哪种姿势理解,对执行结果并没有影响!
弊端:不方便管控,不符合正常逻辑(正常逻辑是,代码一行行的走,而不是让代码插队提前的走)
console.log(a)
let a = 12
过程:
console.log(a)
-> Uncaught ReferenceError: a is not defined
-> 执行到这儿的时候,看到VO(G)里边咩有a
let a = 12
-> 这时VO(G)旗下多了 a = 12
可见区别1:let
声明的变量不存在变量提升
在现在的项目里边,创建函数,一般都是基于函数表达式来实现的,毕竟这可以防止其提前变量提升
如:
fn() // Uncaught ReferenceError: fn is not defined
let fn = function () {
}
一个奇葩的题:
fn();
function fn(){ console.log(1); }
fn();
function fn(){ console.log(2); }
fn();
var fn = function(){ console.log(3); }
fn();
function fn(){ console.log(4); }
fn();
function fn(){ console.log(5); }
fn();
分析:
console.log(5)
-> console.log(3)
fn = function(){ console.log(3); }
测试正确
老师解析:
/*
* EC(G)
* VO(G)
* fn = AAAFFF111
* = AAAFFF222
* = AAAFFF444
* = AAAFFF555 (变量提升阶段完成,fn=AAAFFF555)
* 代码执行
*/
直接忽视掉
var fn
的声明
如:
var x = 12
console.log(window.x) //12
let y = 12
console.log(window.y) //undefined
或许你会疑问,我们在函数里边声明的私有变量,是否也会让函数有个类似 fn.x
这样的属性?
如:
function fn() {
var x = 100
console.log(fn.x) //undefined
console.log(window.x) //undefined
}
fn()
可见,这是咩有的
所以说,这个给GO添加属性的特点,仅限于在全局用var声明创建的变量,而私有EC里边的私有变量,则没这种「嗜好」
还有一种特殊情况:
function fn() {
x = 100
}
// fn里边的x=100,在这里相当于给全局写上 x = 100
分析这种情况:
var/let/const/function
声明的变量,即 x = 100
这样的,并不是 AO(fn)中的私有变量,既然不是,那么就会向上找,即向上找到了全局,而这相当于是给全局VO(G)变量对象中设置了一个x
的全局变量 -> 也相当于是给全局对象GO设置了一个x
的属性当然,这种情况太片门了,我们一般都不会写这样的烂代码,所以理不理解它也没事儿……
既然这个设计是烂的,为何要去记住它呢?
题外话:
在Node.js 环境下:
repl下:
node xx.js
:
a这个变量是被收纳到
1.js
这个模块对象旗下了……而不是全局对象旗下
所以,你要注意,同一个代码,你在浏览器执行,和在Node.js 环境下执行,也可能会不一样,尤其是涉及到 window
这样的全局对象
var这个语法本身就是不严谨的,在编译阶段,浏览器知道它不严谨,所以在面对
var x = 12;var x = 13;
的时候,就睁一只眼闭一只眼了。而let声明的变量则不行,一旦重复声明,代码就不会执行了,直接在变编译阶段给报错了!
可以重复声明情况:
var a = 10
var a = 20
console.log(a)
不允许重复声明情况:
console.log('hi')
let a = 10
console.log(a)
// 你以为执行到这一行才会报错吗?非也……
let a = 20
console.log(a)
我们知道浏览器拿到一份JS代码:
在编译阶段,重复用let声明的变量,就直接GG掉 -> 全部代码也不会被执行
总之:
带var的是可以重复声明的(词法解析审核通过),执行阶段遇到已经声明过的变量,则不会再重新声明
就像是第二次看到
var a
就说了句「咦,已经声明过了呀!既然如此,那就看下一行代码 或者说 类似点到一样,看到重复的学号就不用再点一次了,因为ta已经在了啊!如果再点一次,岂不是多一次一举?」 -> 其实说白了你TM重复声明就不应该出现,太TM不严谨了 -> 当然,如果你是为了容错率,那当我没说哈……
但是let是不可以这样的,这样的代码在词法解析阶段都过不去,所以也就不存在浏览器引擎去执行代码的阶段了
你可以 AST 一下,你会看到:
用let体现代码在执行阶段的报错(可否变量提升?):
console.log('hi')
console.log(x)
let x = 5
从这可以侧面看出,变量提升是在代码执行阶段搞的,而不是在构建AST这个过程搞的!
回过头来看,用let体现编译阶段的报错(重复声明):
一道很常见的面试题:
// 10ms后连续输出五个5,10ms是个理论时间,实际上要比10ms要大
for (var i = 0; i < 5; i++) {
// 定时器是异步操作:不用等定时器到时间,继续开启下一轮循环
setTimeout(() => {
console.log(i);
}, 10);
}
等EC(G)清空后,才把 callback push 到 ECStack 里边去执行
然而,我们的需求并不是连续输出五个5,而是等循环结束后连续输出0~4
不用 let
的情况下,使用闭包来完成这个需求:
// 连续输出0、1、2、3、4
for (var i = 0; i < 5; i++) {
!(function (i) {
// window.setTimeout -> 全局占用了这个 callback heap
setTimeout(() => {
console.log(i);
}, 10);
})(i);
}
为啥会这样呢?
i
的值,都当作实参赋值给这个自执行函数的EC里边的私有变量i
(形参变量)i
值 -> 即用到的i
都是私有EC中保留下来的i
为啥自执行的EC不会被销毁?
i = 0~4
()=>{}
(0x101~0x105
) -> 0x101~0x105[[scope]]:AO(自执行)
window.setTimeout(0x101~0x105,10)
-> 全局API关联了(用到了)callback,而callback隶属于AO(自执行)
-> 所以自执行EC不会被销毁EC(G)执行完毕后?
EC(G)
GG后就入栈执行)scopeChain<AO(0x101),AO(自执行1)>
-> console.log(i)
-> 找到AO(自执行1)
-> 找到i
为0
-> log 0
我之前是不太理解上边这个代码的,而在这篇文章里边,说到:
编程界崇尚以简洁优雅为美,很多时候
如果你觉得一个概念很复杂,那么很可能是你理解错了。
具体点,在我看来的话,如果你觉得一个概念很复杂,那么很可能是你欠缺了很重要的前置知识……当你补上这些前置知识后,你还得需要一些很好例子,来引导你完成对这个「复杂」概念的理解…… -> 难者不会,会者不难
如:如果没有「ECStack、EC(G)、EC(fn)、GO、AO/VO、scope、scopeChain等这样的概念」+「老师在恰当的时机给出的这些例子」,那么我是真得无法在很大程度上理解闭包这个东西的,更不用说理解上边那个代码了
回过头来看,上边这种写法:
所以说,在真实项目里边要尽可能减少对闭包的使用
可是,除此之外,难道还有其它姿势可以满足此需求?
此姿势甚是简单,只需把 var
改成 let
即可:
// 其内部实现原理:跟上边那个闭包原理一模一样……没啥区别
for (let i = 0; i < 5; i++) {
setTimeout(() => {
console.log(i);
}, 10);
}
为啥改成let就可以了呢?而且上边列出的三段代码,一同执行居然没有报错,我本以为用let声明的变量 i
会报错,谁知道居然咩有
因为「let存在块级作用域」呀!而var则是没有的!
具体点来说,就像这样:
{
// -> 父级作用域
let i = 0;
// 第一轮循环
{
// -> 子块作用域
let i = 0;
setTimeout( () => { console.log(i); }, 10);
}
i++; // -> i = 1
// 第二轮循环
{
// -> 子块作用域
let i = 1;
setTimeout( () => { console.log(i); }, 10);
}
// ……同理……
}
可以看到,这跟我们之前讲的闭包没啥区别:
可以看到,这同闭包那套思想是一模一样的,只不过,这里是通过一个let就可以帮我们搞定之前那些麻烦事儿:
for(){}
里边的{}
形成了子块作用域不过,这有一点不好,那就是无法访问在全局访问这个 i
:
用来累积的i是父块作用域的,不是全局的,即你无法在全局使用这个i
所以,let 和 var 的另一个区别是:
题外话,形成块级作用域的(不算对象那个{}
,因为只有key和value,无法声明一个变量,如果你真这样声明一个变量,那么会报语法错误的):
①if(){}
②switch(){case}
即便,你给了break
也会报错:
console.log("hi");
let a = 100;
switch (a) {
case 100:
let x = 200;
break;
case 200:
let x = 300;
default:
break;
}
不过,你这样就不会报错了:
console.log("hi");
let a = 100;
switch (a) {
case 100:
{
let x = 200;
}
break;
case 200:
let x = 300;
default:
break;
}
代码合理情况:
③try{}catch(e){}
不完全归纳法:
除了对象的那个{}
,其它的只要出现了 {}
基本上你使用了let/const
声明的变量就构成了块级作用域
具体体现:
制定规则的人咩有考虑到这种情况
let 与 var有啥区别,那const与var就有啥区别
let 与 const 的唯一区别:
let 创建的变量是可以更改指针指向的(也就是可以重新赋值的),而用const声明的变量是不允许改变指针指向的。
如:
当面试官问用const声明一个常量,不用咬文嚼字;当面试官读Vue为「V-U-E」时,也不用咬文嚼字
EC(G)
的话,那么每一个家庭就是一个EC(fn)
const a = 'hi'
之后,再a = 'xxx'
,那么就会报Uncaught TypeError: Assignment to constant variable
(运行时错误)let a;let a;
-> Uncaught SyntaxError: Identifier 'a' has already been declared
var a
和function a(){}
同时存在,无关顺序 -> function
比var
牛逼,即a
的最终值是函数的地址值fn
-> 地址(0x101
)+[[scope]]:VO(G)
;执行这个函数 -> scopeChain:<AO(fn),VO(G)>
JS的容错率很高,一些其他语言常见的小错误JS都能大度得包容,比如给一个方法传入超出预计的参数、在声明变量之前使用该变量(变量的声明提升解决了这个问题)等等
遇到重复用var声明的变量就跳过之,而遇到函数则定义之
➹:JS变量重复声明以及忽略var 声明的问题及其背后的原理_javascript_SuperCoooooder的博客-CSDN博客
「event loop」 -> 谷歌图片搜索 -> 看到哪个图片好就点进去,顺藤摸瓜找到图片所在的文章……
➹:JavaScript Concurrency Model and Event Loop
➹:What is the Event Loop in JavaScript? - JavaScriptBit
知识是具有延续性的,如果出现大的变故,如战争之类的,就有可能造成知识断层,出现知识不延续现象。
对于国家、社会、世界来说,发生战争,就有可能造成知识断层,出现知识不延续现象。
对于个人,从学校毕业之后,如果连续几年不看报纸、杂志、书籍,不听广播、不看电视中的新闻、综艺等节目,就可能出现个人知识断层,跟不上时代。
在山里隐居……
两个概念:
一个是连续的一片,一个是间隔的不连续的零碎小点
如何解决?
➹:是否会有某个时间人类智商出现断层,有某一代很笨,无法理解现代知识,导致当今人类科研被浪费? - 知乎
➹:中国人社问题─文化不自信导致的价值体系紊乱和知识断层的后果 - 知乎
做任何事情,都要有方法 -> 害怕难的人学不会,会做的人不感到难
做任何事情,都要有方法。如果你知道做某一件事情的最佳方法。那么,你会觉得很好做,一点也不难。困难的事情对于一般人来说,难以解决是因为不懂,而对于会的人或行家来说,一点不难
做任何事情,都要有方法。如果你知道做某一件事情的最佳方法。那么,你会觉得很好做,一点也不难。这就是「会者不难」。同样,如果你不知道做某一件事情的最佳方法,那么,你会觉得很难做。这就是「难者不会」。
举例:
出处:
清·王濬卿《冷眼观》第十二回:“这个就叫做难者不会,会者不难了。我如明明的来伙你去骗人,你又怎能知道我伙人来骗你呢?”
意义:
这就告诉我们要学会建立一种积极的心态,认识困难,承认困难,迎战困难,战胜困难,在解决困难的过程中成长并体验到乐趣与成就感,体会到什么叫做会者不难,这比单单的学习知识练习技能要关键的多,在不断的克服重重困难的过程中,我们的经验知识越来越多,而我们也会有更多的事情会“会者不难”
➹:难者不会,会者不难的意思_拼音是什么_成语解释_造句_近义词_反义词_汉辞网
证明确实有两个作用域:
for循环有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。
可以搞个临时变量:
话说,for是这样,那 if(){}
里边的()
呢?
console.log('hi')
if(let a = 1) {
let a = 5
console.log(a)
}
console.log(a)
语法报错哈,构建AST过程中就GG了!
这就像是在对象里边声明一个变量一样!
➹:let 和 const 命令 - ECMAScript 6入门
感觉有矛盾、理解不够深刻,可以死磕一下……
看完老师讲解后,没看这篇 我用了两个月的时间才理解 let - 知乎 文章的理解,正如这篇文章里边开篇所提到的这样:
说到JS变量,那么就不得不提它有关它的三件事了:
可是let到底有没有提升呢?
undefined
var a
和function a(){}
同时出现(无关先后出现顺序),那么在代码执行之前,a
的值就是函数值,因为函数是有个「赋值」过程,假如function在前,总不能赋值过后又初始化吧?假如var在前,初始化后,然后赋值一个函数值,不是很符合道理吗?至于你用 let a
,在构建AST的过程中就报错了,即「 let 发现重名,叫你滚去改代码」 -> 简单来说「function 比 var 牛逼」const 和 let 只有一个区别,那就是 const 只有「创建」和「初始化」,没有「赋值」过程。
所谓暂时死区,就是不能在初始化之前,使用变量。 -> 这一点是在EC完成的,即你EC入栈的时候,一开始要init一下这个EC的环境,如创建变量啥的,也叫声明变量哈……
题外话:
➹:javascript - Are variables declared with let or const not hoisted in ES6? - Stack Overflow
差不多一个意思!
EC入栈,做两件事:
为啥说是差不多一个意思呢?而不是就是一个意思呢?
Lexical Environments(词法环境),之所以叫词法环境,是因为它是和源程序的结构对应,就是和你所写的那些源码的文字的结构对应,你写代码的时候这个环境就定了。Lexical Environments(词法环境)和四个类型的代码结构相对应:
为啥这就叫词法环境呢? -> 不知道,ES5就是这么规定设计的。。
我们知道只有在全局代码、函数代码、和eval代码三种情况,才会创建运行上下文,而这上边居然会出现有5种?
可以简单认为术语
execution context
视为当前代码被认定为的环境(environment)/作用域(scope),但其实作用域指的是AO/VO
所以这就是词法环境与EC的不同之处呀! -> 前者有5个,而后者有3个
为啥会有不同呢? -> 因为with结构,catch结构,不会创建AO/VO
这样的东西
每次EC入栈,都会有这么两个过程:
scopeChain:<AO(fn),VO(G)>
,对于来说:scopeChain:<VO(G),null>
EnvironmentRecord有两种:
➹:1. 彻底搞懂javascript-词法环境(Lexical Environments) - 掘金
➹:重学js —— Lexical Environments(词法环境)和 Environment Records(环境记录) · Issue #49 · lizhongzhen11/lizz-blog
➹:图解JS词法环境「Lexical environment」_javascript_public class Me的博客-CSDN博客