# 你不知道的 JavaScript(上)
# 第一部分
# 第 1 章 作用域是什么
# 编译原理
在传统编译语言的流程中,程序中的一段源代码在执行之前会经历三个步骤,统称为“编译”
- 分词/词法分析 分单词
- 解析/语法分析 ast
- 代码生成 分配内存
# 理解作用域
参与者
- 引擎 从头到尾负责整个 JavaScript 程序的编译及执行过程。
- 编译器 引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。
- 作用域 引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值
LHS 和 RHS
function foo(a) {
console.log(a)
}
foo(2)
// RHS 查找一个马上要用的值 查找
// LHS 需要把一个值放到一个地方 赋值
https://www.jianshu.com/p/ecb1c72fd902
# 作用域嵌套
引擎从当前的执行作用域开始查找变量,如果找不到,就向上一级继续查找。当抵达最外层的全局作用域时,无论找到还是没找到,查找过程都会停止。 LHS 和 RHS 引用都会在当前楼层进行查找
# 异常
为什么区分 LHS 和 RHS 是一件重要的事情?
因为当无法找到变量时 处理结果可能不一致
在 非严格模式/严格模式 下 如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常 在 非严格模式下 当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量,全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎 在 非严格模式下 LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出同 RHS 查询失败时类似的 ReferenceError 异常
ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对结果的操作是非法或不合理的。
# 第 2 章 词法作用域
作用域共有两种主要的工作模型
词法作用域
动态作用域
# 词法阶段
什么叫词法?
编译器的第一个工作阶段叫作词法化 词法化的过程会对源代码中的字符进行检查,如果是有状态的解析过程,还会赋予单词语义
词法作用域就是定义在词法阶段的作用域
词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域不变(大部分情况下是这样的)
无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
# 欺骗词法 欺骗词法作用域会导致性能下降
- eval JavaScript 中的 eval(..)函数可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。
setTimeout setInterval new Function() 都可以接收一个代码字符串 来运行一段代码
在程序中动态生成代码的使用场景非常罕见,因为它所带来的好处无法抵消性能上的损失
- with
总结
词法作用域意味着作用域是由书写代码时函数声明的位置来决定的。编译的词法分析阶段基本能够知道全部标识符在哪里以及是如何声明的,从而能够预测在执行过程中如何对它们进行查找。
# 第 3 章 函数作用域和块作用域
# 函数中的作用域
属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。这种设计方案是非常有用的,能充分利用 JavaScript 变量可以根据需要改变值类型的“动态”特性
# 函数作用域
区分函数声明和函数表达式最简单的方法是看 function 关键字出现在声明中的位置(不仅仅是一行代码,而是整个声明中的位置)。如果 function 是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
函数声明和函数表达式之间最重要的区别是它们的名称标识符将会绑定在何处。
函数表达式可以是匿名的,而函数声明则不可以省略函数名——在 JavaScript 的语法中这是非法的
# 块作用域
表面上看 JavaScript 并没有块作用域的相关功能 var
with
同时也是块作用域的一个例子(块作用域的一种形式),用 with 从对象中创建出的作用域仅在 with 声明中而非外部作用域中有效。
try/catch
会创建一个块作用域,其中声明的变量仅在 catch 内部有效。
let
let 为其声明的变量隐式地劫持了所在的块作用域
const
用来创建块作用域变量,但其值是固定的(常量)。之后任何试图修改值的操作都会引起错误
# 第 4 章 提升
作用域同其中的变量声明出现的位置有某种微妙的联系
包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
- 函数声明直接提前
一个普通块内部的函数声明通常会被提升到所在作用域的顶部
函数优先 函数声明比赋值优先 函数重复声明会覆盖 函数和普通声明已函数声明为准
总结
我们习惯将 var a = 2;看作一个声明,而实际上 JavaScript 引擎并不这么认为。它将 var a 和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。
这意味着无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。
声明本身会被提升,而包括函数表达式的赋值在内的赋值操作并不会提升。
要注意避免重复声明,特别是当普通的 var 声明和函数声明混合在一起的时候,否则会引起很多危险的问题!
# 第 5 章 作用域闭包
谈闭包 先要谈作用域
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行
一个函数执行完成以后 在某个时间 另外一个函数执行会用到第一个函数内部的作用域变量 这就是闭包
只要使用了回调函数,实际上就是在使用闭包! 因为回调的作用就是等待被触发 触发就会使用当时函数作用域
# 循环和闭包
for (var i = 1; i <= 5; i++) {
setTimeout(function () {
console.log(i)
}, i * 1000)
}
缺陷是我们试图假设循环中的每个迭代在运行时都会给自己“捕获”一个 i 的副本。但是根据作用域的工作原理,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i。
立即执行函数会自己形成一个词法作用域
https://weread.qq.com/web/reader/8c632230715c01a18c683d8ke3632bd0222e369853df322
# 模块
如果要更简单的描述,模块模式需要具备两个必要条件。
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
TIP
回调 和 模块 是 闭包的两大模式
# 动态作用域
主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。(this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。
词法作用域 静态作用域 书写的时候就确定了变量的一个查找方式
this 动态 需要按照运行时候调用的情况去分别找到上下文
# 第二部分
# 第 1 章 关于 this
this 的作用就是 减少函数传参上下文对象??
this 提供了一种更优雅的方式来隐式“传递”一个对象引用,因此可以将 API 设计得更加简洁并且易于复用。
显式传递上下文对象会让代码变得越来越混乱,使用 this 则不会这样。当我们介绍对象和原型时,你就会明白函数可以自动引用合适的上下文对象有多重要
每当你想要把 this 和词法作用域的查找混合使用时,一定要提醒自己,这是无法实现的
# this 到底是什么
this 是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
this 实际上是在函数被调用时发生的绑定,它指向什么完全取决于函数在哪里被调用。
每个函数的 this 是在调用时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法)
# 第 2 章 this 全面解析
# 绑定规则
默认绑定 独立函数调用 指向全局 严格模式下 LHS 会失败无法查询 this 导致 TypeError: this is undefined
隐式绑定 调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含,不过这种说法可能会造成一些误导
显示绑定 call apply bind
new 绑定
实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用
- 箭头函数的 this 指向他的词法作用域
# 绑定例外
显性绑定 第一个值 null
间接引用
(a.foo = b.foo)()
函数的赋值返回函数 默认绑定来说,决定 this 绑定对象的并不是调用位置是否处于严格模式,而是函数体是否处于严格模式。如果函数体处于严格模式,this 会被绑定到 undefined,否则 this 会被绑定到全局对象。
# 第 3 章 对象
创建对象的方式
- 字面创建 {a:123}
- 构造形式 new Object()
基本数据类型
string boolean number null undefined symbol bigint
object
内置对象
String Number Boolean Object Function Array Date RegExp Error
# 内容
# 深浅拷贝
# 属性描述符
- value
- writable writable 决定是否可以修改属性的值。
- enumerable
- configurable
# 存在性
in 操作符会检查属性是否在对象及其[[Prototype]]原型链中
hasOwnProperty(..)只会检查属性是否在 myObject 对象中
但是它实际上检查的是某个属性名是否存在。对于数组来说这个区别非常重要
# 可遍历性
在数组上应用 for..in 循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举属性。最好只在对象上应用 for..in 循环,如果要遍历数组就使用传统的 for 循环来遍历数值索引
# 第 4 章 混合对象“类”
# 第 5 章 原型
# 第 6 章 行为委托
**[[Prototype]]**机制就是指对象中的一个内部链接引用另一个对象
JavaScript 中这个机制的本质就是对象之间的关联关系
# 面向委托设计
Foo = {
init: function (who) {
this.me = who
},
identify: function () {
return 'I am ' + this.me
},
}
Bar = Object.create(Foo)
Bar.speak = function () {
alert('Hello' + this.identify())
}
var b1 = Object.create(Bar) // b1的原型对象是Bar b1.__proto__===Bar
b1.init('b1')
var b2 = Object.create(Bar)
b2.init('b2')
b1.speak
b2.speak
# 类与对象
把类当做一个对象来处理
使用对象关联风格来编写代码时不需要把 Widget 和 Button 当作父类和子类。相反,Widget 只是一个对象,包含一组通用的函数,任何类型的控件都可以委托,Button 同样只是一个对象。(当然,它会通过委托关联到 Widget!)
在委托设计模式中,除了建议使用不相同并且更具描述性的方法名之外,还要通过对象关联避免丑陋的显式伪多态调用(Widget.call 和 Widget.prototype.render.call),代之以简单的相对委托调用 this.init(..)和 this.insert(..)。
var father = {
init: function (name) {
this.name = name
},
}
var son = Object.create(father)
son.introduce = function () {
console.log(`my name is ${this.name}`)
}
p1 = Object.create(son)
p1.init('liang')
p1.introduce()
# 更简洁的设计
对象关联风格代码和行为委托设计模式的
# 小结
行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制。也就是说,我们可以选择在 JavaScript 中努力实现类机制(参见第 4 和第 5 章),也可以拥抱更自然的 [[Prototype]] 委托机制。
当你只用对象来设计代码时,不仅可以让语法更加简洁,而且可以让代码结构更加清晰。对象关联(对象之前互相关联)是一种编码风格,它倡导的是直接创建和关联对象,不把它们抽象成类。对象关联可以用基于[[Prototype]]的行为委托非常自然地实现。