第一部分 作用域和闭包
第1章 作用域是什么
编译原理
在传统编译语言的流程中,一段源代码的执行会经历三个步骤
- 分词/词法分析(Tokenizing/Lexing)
- 解析/语法分析(Parsing)
- 代码生成
理解作用域
-
引擎
从头到尾负责整个JavaScript程序的编译及执行过程。
-
编译器
负责语法分析及代码生成等脏活累活。
-
作用域
负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就对它赋值。
引擎在查找变量时会进行LHS或者RHS查找,含义是赋值操作的左侧和右侧。
作用域嵌套
在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(全局作用域)为止。
异常
ReferenceError
同作用域判别失败相关,而TypeError
代表作用域判别成功了,但对结果的操作是非法或不合理的。
第2章 词法作用域
词法阶段
词法作用域就是定义词法阶段的作用域。词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的。
作用域查找会在找到第一个匹配的标识符时停止。
欺骗词法
-
eval()
函数可以用来欺骗词法作用域,修改运行期所在的词法作用域。严格模式下,
eval()
有运行时有其自己的词法作用域,意味着无法修改所在的作用域。function foo(str, a) { eval(str) // 欺骗! console.log(a, b) } var b = 2 foo('var b = 3', 1); // 1, 3
-
with
通常被当做重复引用同一个对象中的多个属性的快捷方式,可以不需要重复引用对象本身。根据你传递给它的对象凭空创建了一个全新的词法作用域。
-
严格模式
eval()
和with
都会被限制
使用eval和with会严重运行JavaScript的执行性能
第3章 函数作用域和块作用域
函数中的作用域
含义:属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
隐藏内部实现
最小限度地暴露必要内容,而将其他内容都“隐藏”起来。
function doSomething(a) {
function doSomethingElse(a) {
return a - 1
}
var b
b = a + doSomethingElse(a * 2)
console.log(b + 3)
}
doSomething(2) // 8
规避冲突
- 使用命名空间
- 模块管理
函数作用域
如果function
是声明中的第一个词,那么就是一个函数声明,否则就是一个函数表达式。
任何声明在某个作用域内的变量,都将附属于这个作用域。
匿名函数缺点:
- 追踪调试很困难
- 引用自身职能使用已经过期的argument.callee
- 省略了对于代码可读性/可理解性很重要的函数名
由一对括号()包裹就成为了一个表达式
IIFE =》 立即执行函数表达式
(function(){...})()
和(function() {...}())
功能一致
IIFE用途:
- 把他们当做函数调用并传递参数进去
- 倒置代码的运行顺序
块作用域
拥有块作用域:
-
with
-
try/catch
-
let
let为其声明的变量隐式地劫持了所在的块作用域。通常是({…})
垃圾回收
function process(data) {...} //在这个块中定义的内容完事可以销毁 { let someReallyBigData = {...} process(someReallyBigData) }
let循环
for(let i = 0; i < 10;i++) { console.log(i) } console.log(i) // ReferenceError
-
const
第4章 提升
包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
var a = 2
会被看成var a;和a=2;第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。
先有声明后有赋值。每个作用域都会进行提升操作。
函数声明会被提升,函数表达式不会。
函数会首先被提升,然后才是变量。
第5章 作用域闭包
闭包使得函数可以继续访问定义时的词法作用域。
闭包与作用域关联。
模块模式需要具备的两个条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
动态作用域
JavaScript并不具有动态作用域,它只有词法作用域
词法作用域和动态作用域主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定的。
第二部分 this和对象原型
第1章 关于this
this
是在运行时进行绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。
this
的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会创建一个执行上下文。这个记录会包含函数在哪里被调用、函数的调用方式、传入的参数等信息。this
就是这个记录的一个属性,会在函数执行的过程中用到。
第2章 this全面解析
调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
四种this情况
-
默认绑定(独立函数调用)
this指向全局对象
严格模式下this指向undefined
-
隐式绑定
函数调用中的this绑定到这个上下文对象。
对象属性引用链中只有上一层或者说最后一层在调用位置中起作用。
回调函数丢失this绑定是非常常见的。
-
显示绑定
call()、bind()、apply()
强绑定
-
new绑定
实际上不存在所谓的“构造函数”,只有对于函数的“构造调用”。
new过程发生了什么:
- 创建一个全新的对象
- 将函数的[[Prototype]]指向这个新对象
- 新对象会绑定函数调用的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
优先级:
polyfill
代码主要用于旧浏览器的兼容。
默认绑定<隐式绑定<显示绑定<new绑定
bind()
函数可以把除了第一个参数(用于绑定this)之外的其他参数都传给下层的函数。
判断this
- 函数是否在new中调用?(new绑定)
- 函数是否通过call、apply或者硬绑定调用?
- 函数是否在某个上下文对象中调用?
- 如果都不是的话,使用默认绑定。如果在严格模式下,就绑定到undefined,否则绑定到全局对象。
绑定例外
- 被忽略的this
如果把null或者undefined作为this的绑定对象传入call、apply或者bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
Object.create(null)和{}很像,但是并不会创建Object.prototype这个委托,所以它比{}“更空”
-
间接引用
你有可能有意无意地创建了一个函数的“间接引用”,这种情况下调用这个函数会应用默认绑定规则。
对于默认绑定来说,决定this绑定对象的是函数体是否处于严格模式。
-
软绑定
if (!Function.prototype.softBind) { Function.prototype.softBind = function(obj) { const fn = this; // 捕获所有curried参数 const curried = [].slice.call(arguments, 1); const bound = function() { return fn.apply((!this || this === (window || global)) ? obj : this, curried.concat.aplly(curried, arguments)); } bound.prototype = Object.create(fn.prototype); return bound; } }
this词法
箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用域来决定this。
箭头函数的绑定无法被修改(new也不行!)。
这其实和ES6之前代码中的self = this机制一样。
第3章 对象
语法
对象可以通过两种形式定义:声明形式和构造形式
类型
JavaScript中一共有六种主要类型
- string
- number
- boolean
- null
- undefined
- ojbect
简单类型本身并不是对象,null有时会被当做一种对象类型,但是这其实只是语言本身的一个bug,即对null执行typeof null是会返回字符串’object‘。实际上,null本身是基本类型。
内置对象
- String
- Number
- Boolean
- Object
- Function
- Array
- Date
- RegExp
- Error
在必要时语言会自动把字符串字面量转换成一个String对象。数字字面量也同理。
let strPrimitive = 'i am a strging';
console.log(strPrimitive.length); // 13
console.log(strPrimitive.charAt(3)); // 'm'
null和undefined没有对应的构造形式,它们只有文字形式。相反,Date只有构造,没有文字形式。
对于Object、Array、Function和RegExp来说,无论使用文字形式还是构造形式,它们都是对象,不是字面量。
内容
在引擎内部,这些值的存储方式是多种多样的,一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,它们就像指针一样,指向这些值真正的存储位置。
“函数”和“方法”在JavaScript中是可以互换的。
即使你在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象。
数组
数组期望的是数值下标,也就是说值存储的位置(索引)是非负整数。
复制对象
JSON.parse(JSON.stringify(obj))实现深拷贝。
Ojbect.assign(…)方法可以实现浅复制。
属性描述符
-
writable(可写)
是否可以修改属性的值。
-
enumerable(可枚举)
属性是否出现在对象的属性枚举中。
-
configurable(可配置)
是否可以使用defineProperty(…)方法来修改属性描述符,是否可以删除这个属性。修改成false是单向操作,无法撤回。
不变性
属性或者对象不可改变方法:
-
对象常量
结合writable:false和configurable:false就可以创建一个真正的常量属性。
-
禁止拓展
使用Object.preventExtensions(…)。
-
密封
使用Object.seal(…)会创建一个“密封”的对象,会在一个现有对象上调用Object.preventExtensions(…)并把所有现有属性标记为configurable:false。
-
冻结
Object.freeze(…)会创建一个冻结对象,会在一个现有对象上调用Object.seal(…)并把所有“数据访问”属性标记为writable:false。
Getter和Setter
getter会在获取属性值时调用。setter会在设置属性值时调用。
尽量成对出现。
存在性
in操作符会检查属性是否在对象及其[[Prototype]]原型链中。
hasOwnProperty(…)只会检查属性是否在对象中,不会检查原型链。
枚举
Object.keys(…)会返回所有可枚举属性组成的数组。
Object.getOwnPropertyNames(…)会返回所有属性无论它们是否可枚举组成的数组。
遍历
遍历对象属性时的顺序是不确定的,在不同的JavaScript引擎中可能不一样。
for…of会寻找内置或者自定义的@@iterator对象并调用它的next()方法来遍历数值。
普通对象没有内置@@iterator,所以无法自动完成for…of遍历。
第4章 混合对象“类”
面向类的设计模式:实例化、继承和多态。
其他语言中的类和JavaScript中的“类”并不一样。
类的继承其实就是复制。
多态看起来似乎是从子类引用父类,但是本质上引用的其实是复制的结果。
第5章 原型
JavaScript中只有对象。
JavaScript中继承会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。
函数不是构造函数,当且仅当使用new时,函数调用会变成“构造函数调用”。
constructor属性在[[prototype]]原型对象上,不在实例对象。
.constructor不是一个不可变属性,它是不可枚举的,但是值是可被修改的。所以要尽量避免使用。
// 两种把Bar.prototype关联到Foo.prototype的方法
// 1.ES6之前需要抛弃默认的Bar.prototype
Bar.prototype = Object.create(Foo.prototype)
// 2.ES6开始可以直接修改现有的Bar.prototype
Object.setPrototypeOf(Bar.prototype, Foo.prototype)
isPrototypeOf检验一个对象是否在另外一个对象的原型链上。
__proto__看起来很像一个属性,实际上它更像一个setter/getter
.__proto__的实现大致过程
Object.defineProperty(Object.prototype, '__proto__', {
get() {
return Object.getPrototypeOf(this)
},
set(o) {
Object.setPrototypeOf(this, 0)
return o
}
})
Ojbect.create(null)会创建一个拥有空[[prototype]]链接的对象,这个对象无法进行委托。
Object.create的polyfill代码
if(!Object.create) {
Object.create = function(o) {
function F() {}
F.prototype = o
return new F()
}
}
[[prototype]]就是_proto_
JavaScript中机制有一个核心区别就是不会进行复制,而是通过[[prototype]]链关联。
[[prototype]]机制就是指对象中的一个内部链接引用另外一个对象。
第6章 行为委托
委托行为
const Task = {
setID(ID) { this.id = ID }
outputID() { console.log(this.id) }
}
const XYZ = Object.create(Task)
XYZ.prepareTask = function(ID, Label) {
this.setID(ID)
this.label = label
}
// ABC = Object.create(Task)
// ...
第三部分 中卷
第五章 程序性能
5.1 Web Worker
浏览器(即宿主环境)的功能,跟JavaScript本身没关系。
多线程方式运行。
const w1 = new Worker("http://some.url.1/mycoolworker.js")
通过事件订阅/推送机制通信。
Worker环境
Worker内部无法访问主程序的任何资源,不能访问它的任何全局变量,也不能访问页面的DOM或者其他资源。
应用:
- 处理密集型数学计算
- 大数据集排序
- 数据处理(压缩、音频分析、图像处理等)
- 高流量网络通信
共享Worker
new sharedWorker('./01.js')
通过port对象进行通信
w1.port.postMessage("something cool")
5.2 SIMD
单指令多数据(SIMD)是一种数据并行方式。