之前被好友推荐过《你不知道的JavaScript》这一系列的书籍。上中下三本书都不算厚,内容也是比较独立。本来是打算一个月内看完这本上卷和下卷,可总是因为自己的懒惰读了一个月才断断续续读完上卷。不得不说单是粗略的读完上卷就让我很有收获,下面是对上卷部分的读书笔记,不是什么文档和教程,仅仅供自己学习记录,欢迎各位路过的大佬能指出理解错误的地方以及意见。
第一部分 作用域和闭包
第1章 作用域是什么
1.1编译原理
首先,我也是学习JavaScript一开始,找到资料里面就说JavaScript是一门“解释型”语言。但是其实JavaScript是一门编译语言。
在传统的编译语言中,一段源代码执行前会先经过一段叫做编译的操作,而编译一共有如下3个过程。
- 分词/词法分析(Tokenizing/Lexing)
这个过程将把编程语言的字符串分解为有意义的代码块,这些代码块被称为词法单元(token)
var a = 2;
//就会被拆解成 var、a、=、2、; 这些都是词法单元
//而空格是否属于此法单元取决于是否有意义,
//这里的var a = 2;中的任何一个空格都没有意义
- 解析/语法分析(Parsing)
把众多词法单元组成的词法单元流(数组)转换成一颗树的结构,叫抽象语法树(AbstractSyntaxTree,AST)
var a = 2;
//这句语句转化为树的结构是一个叫做VariableDeclaration
- 代码生成
将语法树转化为可执行的代码过程。
其他的语言的编译在构建前,而JavaScript在执行前也会进行这编译3部曲
1.2理解作用域
对var a = 2;处理时候,具体的编译器是这样工作的。
//1 遇到 var a 编译器会在当前作用域中的一个集合查询是否有a这个变量,
//如果有就继续进行第2步编译动作,
//如果没有会要求当前作用域在自己的这个集合中声明一个新的a变量
//2 遇到 a = 2 编译器为这条语句生成运行时候的代码,供运行时候使用。
//这时候编译器要在生成的代码中表达出 a=2这个意思,所以在生成代码时候,
//需要一个a变量,先去找当前的作用域中的集合是否含有这个变量,
//如果没有就去上一层找。直到找到为止,否者就是报错。
在这里注意的是在步骤2中,编译器会查找作用域集合中时候会有2种不同的查找方式,一种是LHS,一种是RHS。也就是左查询与右查询。这个和我在学习C++中左值右值时候,我觉得十分相识。也就是通过最原始的“=”进行理解,左边是被填充的,右边是去填充的。
var a = 2;//这里编译器生成代码时候,会去当前作用域集合中找a变量,然后赋值为2。
//你可以理解为是找到这片叫做a空间,然后填充上2这个数据,
//所以这里是左查询,也就是找到被填充的a空间
console.log(a)//这里编译器生成代码时候,会去当前作用域集合中找a变量,然后打印出来。
//你可以理解为是找到这个叫做a的变量里面的值,然后使用这个值,
//所以这里是右查询,也就是找到去填充(或者理解为取出来使用)的a空间里面本身的值!!!
1.3作用域嵌套
其实书中这部分的内容讲的就是作用域链,也就是对var a = 2;处理时候,具体的编译器工作的第二步中需要一个a变量,先去找当前的作用域中的集合是否含有这个变量,如果没有就去上一层找。直到找到为止,直到最顶层没有报错为止。这就是一直往上的查找就是顺着作用域链。
var b = 3
function foo(a){
console.log(a+b)
}
foo(2)//4
//这里的function内部是自己的函数作用域,function外面是全局作用域。
//function内部作用域没有b变量,为什么还能具体工作?
//就是因为function中没有,所以找上一级,然后上一级中就有b,就可以使用
//这种机制就是作用域嵌套,或者作用域链。
//我反正是理解为,子作用域可以使用父作用域,但是父作用域不可以反过来使用子作用域。
//当然这里的什么父、子作用域是个人为了理解,自己说的,不是什么术语。
1.5异常
前面都是顺着作用域链能查找成功的时候,但是我们也说过,如果找不到会报错。这里我们查找有2中情况。自然左右查找不到,报错是不一样的。
这里小小验证一下书上讲的左查询吧。
a = 2//这里没有var 声明。但是a=2是左查找,所以左查询热心的会在全局作用域中的集合里面创建一个空的a变量空间。
console.log(a)//右查找结果是2
"use strict"
a = 2//不好意思,这次左查询无法帮助生成一个空的a变量空间了,
//单单是这一句语句就会因为左查询失败,报ReferenceError
console.log(a)//右查找,肯定失败,这句本生也是右查找失败报ReferenceError,
//不过前一句本来就是错,压根也不会运行到这一句。
第2章 词法作用域
2.1词法阶段
首先词法作用域就是你写代码时候,书写的变量位置或者块作用域的位置就是词法作用域。而这个词法分析时候(也就前面的拆分语句var a = 2;为var、a、=、2、;),产生的作用域和写的时候一样。例如如下代码。
var a = 3;//这行语句是属于全局作用域的
function foo(a) {
//这里面是foo函数形成的块作用域
//写所以下面的b,bar(),以及自己的a。都是这个作用域的
//所以解析之后任然是满足这个关系
var b = a * 2;//这里的a是foo函数里面的,不是外面的var a = 3;里面的a
function bar(c) {
//这里面是bar函数形成的块作用域
//所以自己的c是属于这里
console.log(a,b,c);
}
bar(b * 3);
}
foo(2);//2,4,12
所以词法作用域主要是体现了2点:
- 一是一层一层往上的循着作用域找变量,这个案例中就是bar使用的a,b,c来自上一级的foo。
- 二是,当子级的作用域有着与父级作用域同样的标识符(就是变量名、函数名)时候,子级的会覆盖(如果是知道方法重写,那么这里就理解为子类重写父类方法),这种叫做遮蔽效应。例如在foo作用域、以及子作用域中使用a时候,都是foo函数中的a,而不是foo作用域的父作用域中的var a = 3;中的a。当然,当bar中有a时候,bar中使用a时候,就会使用自己a,而覆盖foo(a)的a,与全局的var a = 3的a
2.2欺骗词法
首先记住,少使用欺骗词法,欺骗词法作用域会导致性能下降。使用eval与with关键字能实现这种。我这里就写个eval
首先eval()的用法是接受一串字符串,并把字符串的内容替换到调用的位置,有点抽象,请看代码:
function foo(str,a) {
//这里是foo形成的词法作用域
//先假装eval(str)不存在
//这里只有a与str是自己的,b是外面的
//所以调用foo("var b = 3;",1);应该是1,2的结果
//放开注释eval(str)
//输出为1,3
//因为eval("var b = 3;")等同于在这行eval位置替换为
//var b = 3;
//因此var b = 3中的b屏蔽外面的b
//eval(str);
console.log(a,b);
}
var b =2;
foo("var b = 3;",1);//1,3
第3章 函数作用域和块作用域
3.1函数中的作用域
- 函数作用域:属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)这种设计方案能充分利用JavaScript变量可以根据需要改变值类型的“动态”特性。
3.2隐藏内部实现
- 最小暴露原则:在软件设计中,应该最小限度的暴露最少内容,而将其他内容隐藏起来。
- 在JavaScript中的函数作用域就可以实现隐藏代码的效果
function doSomething(a) {
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
function doSomethingElse(a) {
return a - 1;
}
var b;
doSomething(2);//15
很显然这和时候我们任然能够访问到b以及doSomethingElse()函数,但是目前就这些代码而言,b以及doSomethingElse()只是为了给doSomething()使用。因此我们不应该让外部也能够访问到这两个标识符。所以应该进行隐藏
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
doSomething(2);//15
3.3函数作用域
尽管前面的使用函数包裹代码块达到了隐藏的意义,但是在某些时候任然是有一些缺点。一是用来包裹隐藏的函数名污染了全局变量,然后必须显示的调用函数名。但是幸好我们可以使用立即执行函数来解决。
(function foo() {
var a = 3;
console.log(a);
})();
console.log(a);
//这里就可以让函数当作一个表达式,而不是函数声明
//这里就没有让foo放出去污染全局,因为他不是声明
//这里最后紧紧跟了一个(),表示立即调用,因此不用显示使用标识符调用
//当然把最后的()放进去的效果也是一样的,仅仅是书写风格不同。如下
(function foo() {
var a = 3;
console.log(a);
}());
console.log(a);
- 那么时候是函数表达式呢?
Good question
区分:书上的办法很简单,就是查看function关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果function是声明中的第一个词,那么就是一个函数声明,否者就是一个函数表达式。
上面的代码中(function foo(){…})()就不是function开头,这个时候就是作为表达式,而foo标识符就是被封锁在foo(){…}中…的部分中,所以就没有污染全局变量
- 匿名和具名
匿名函数就是没有名字的函数
setTimeout(function(){
console.log("I waited 1 second");
},1000);
这部分的片段代码中seTimeout中function()就是没有名字。所以就是匿名函数。
- 注意:之后函数表达式才能拥有这种匿名函数,函数表达式没有函数名。请看前面的代码,这里可是函数表达式哈。
3.4块作用域
现在我们学会了词法作用域、函数作用域以及一些小的知识点。现在学习新的作用域、块作用域。
前面说过,使用函数作用域能够隐藏一段代码,而隐藏也就是具有隔离意思。把一段代码隔离到一个函数中,让其外界无法触碰。但是并不是使用外包函数就能解决。
//这行代码只是为了验证()中的var i,以及{}中的a,最后在外部均可以访问到这两个标识符
for(var i = 0; i < 10; i++){
console.log("for里面调用i",i);
var a = 5;
}
console.log("for外面调用for里面的a",a); //for外面调用for里面的a 5
console.log("for外面for里面的i",i);//for外面for里面的i 10
下面是使用函数包裹
(function(){
for(var i = 0; i < 10; i++){
console.log("for里面调用i",i);
var a = 5;
}
}())
console.log("for外面调用for里面的a",a);//VM22:7 Uncaught ReferenceError: a is not defined
console.log("for外面调用i",i);//Uncaught ReferenceError: i is not defined
是完成了封锁,可是这样情况并不简便,所以开发者为JavaScript提供了块作用域。我觉得其实就和完成了函数作用域一样的功能,封锁代码块在其{}中。
- let
let声明的变量,是会和当前的作用域进行绑定。
for(let i = 0; i < 10; i++){
console.log("for里面调用i",i);
var a = 5;
}
console.log("for外面for里面的i",i);//Uncaught ReferenceError: i is not defined
恭喜,i已经绑定在for体内了,是不是方便很多了呢。
注意let i,不仅仅是把i绑定到了for循环的{}中,准确的说是绑定到了每一个迭代中,这点十分十分重要。后面闭包时候会再次提到这个事。同时let声明的标识符不会进行提升,这一点后面的提升相关部分会进行说明
-
const
const声明的变量,是会和当前的作用域进行绑定,并且这个变量的值是固定的,不能再更改 -
try/cath
没想到吧,居然每一个cath分支都是一个块作用域,不相信的话,可以动手试一试
第4章 提升
首先得出结论:
- 只有声明本身会被提升,而赋值或者其他运行的逻辑会留在原地。如果是提升改变的代码执行的顺序,会造成严重的破坏
- 声明包括变量声明以及函数声明
- 每个作用域都会进行提升
- 函数和变量提升时候,函数会优先。因为函数是一等公民
- let以及const声明的标识符不会提升
foo();//1
var foo;
function foo() {
console.log(1);
}
foo = function () {
console.log(2);
}
上面代码会被引擎理解为如下形式:
function foo() {
console.log(1);
}
foo();//1
foo = function () {
console.log(2);
}
foo();//2
所以函数是优先提升的。
第5章 作用域和闭包
5.1 总结
我喜欢开篇,直接就总结完。
闭包产生的2种情况
- 当函数作为另一个函数的参数
- 函数作为返回值返回
function foo() {
var a = 2;
function bar() {
console.log(a);
}
return bar;
}
var baz = foo();
baz();//2 朋友,这就是闭包效果
5.2 循环和闭包
要说明闭包,for循环是一个常见的例子
for ( i = 1; i <= 5; i++) {
setTimeout(function timer(){
console.log(i);
}, i*1000);
}
这段代码再运行时候会每秒一次的频率输出5次6.
延迟函数的回调会在全部循环迭代结束的之后时候进行调用(请查询宏任务、微任务相关知识点),而不是每次迭代时候调用。所以最后调用i,但是i是公共的,并且值为最后一个循环决定的6。所以结果是5次6
那怎么给每个迭代的版本获取一个实时的i,满足哪怕是最后循环迭代完再调用定时函数,但是每个定时函数都是调用自己版本的,而不是调用最后的公用6呢?
那就是每次循环迭代时候,我们给每一个迭代都绑定一个i。如下所示,我们使用let让每一个i都再内部迭代进行绑定。
for(let i = 1; i <= 5; i++){
setTimeout(function timer(){
console.log(i);
},i*1000);
}
5.3 模块
在js中的模块也是和闭包息息相关。
模块:
- 必须要有外部的封闭函数,该函数必须要被调用一次
- 封闭的函数至少要返回一个内部函数
- 使用立即执行函数配合有奇效
var foo = (function(){
var something = "cool";
var another = [1,2,3];
function doSomething() {
console.log(something);
}
function doAnother() {
console.log(another.join("!"));
}
return {
doSomething,
doAnother
}
})();
foo.doSomething();//cool
foo.doAnother();//1!2!3
第二部分 this和对象原型
第1章 关于this
第2章 this全面解析
第3章 对象
第4章 混合对象“类”
第5章 原型
第6章 行为委托
未完待续,下个周六整理完第二部分