第一章
编译原理
js是一门编译语言
传统编译语言流程:
- 分词/词法分析:把字符串分解成有意义的代码块
- 解析/语法分析:把词法单元流(数组)转换成一个由元素逐级嵌套所组成的“抽象语法树”
- 代码生成
js编译:
编译过程不是发生在构建之前的,而是发生在代码执行前的几微秒内
理解作用域
- 引擎:负责整个js程序的编译和执行
- 编译器:负责语法分析及代码生成(脏活累活)
- 作用域:负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
变量的赋值操作:
步骤一:编译器在当前作用域中声明一个变量(如果之前没声明过)
步骤二:运行时,引擎在作用域中查找该变量,如果可以找到就对它赋值
引擎对变量进行LHS查询,另一个查找类型叫RHS查询----L代表左侧,R代表右侧
指的是赋值操作的左侧和右侧,即
变量出现在赋值操作的左侧时进行LHS操作,出现在右侧时进行RHS操作
RHS
- consol.log(a)时,对a进行RHS查询,问,a的值是多少(retrieve his sourse value)---
- b =2 ; 对a进行LHS查询,问,a是神马----
1.2.5的小测验
function foo(a){
var b = a;
return a + b;
}
var c = foo(2);
执行过程:
- 对foo(2)进行RHS查找,执行foo(2)
- a =2 对a 进行LHS ,进入函数,
- var b = a ,对a进行RHS查找 ,然后对b进行LHS查找
- return a + b 对a 和b各进行一次RHS,退出函数
- var c=foo(2);对c进行一次LHS查找
总计,3次LHS,4次RHS
- 作用域嵌套查找
区分RHS和LHS的意义:
RHS失败会抛出 ReferenceError 引用异常
LHS失败会自动隐式的创建全局变量(非严格模式下)
RHS之后如果尝试对变量进行不合理的操作,会抛出TypeError,这表明作用域判别成功,但是对结果的操作不合法
第二章
词法作用域:定义在词法阶段的作用域---
作用域气泡
- 作用域查找:会在第一个匹配的标识符处停止,因此多层嵌套可以定义同名的标识符,这叫“遮蔽效应”
全局变量会自动成为全局对象(比如widow)的属性,因此可以通过window.a来访问那些被同名变量所遮蔽的全局变量
欺骗词法-(性能会下降)
-
eval( ) 把内部字符串视为,好像书写时就存在于那里----动态插入(严格模式中eval()有自己的词法作用域,无法修改所在的作用域)
setTimeout(...)和setInterval(...)第一个参数可以被解释成动态生成的函数代码,new Function(...)也很类似最后一个参数可以接受代码字符串,并转化成动态生成的函数,比eval安全一些
-
with 不需要重复引用对象本身
with语句内部,当对一个不包含某属性的对象进行with时,会创建一个全局作用域的变量属性(非严格模式),严格模式完全禁止with
影响引擎优化的处理方案,没有eval()和with,引擎依赖代码的词法进行静态分析,预先确定变量和函数的定义位置,但是如果有eval()和with,有关标识符位置的判断会被判断为无效的
第三章
- 函数作用域 属于这个函数的全部变量都可以在整个函数的范围内使用和复用,在嵌套的作用域中也可以
- 基于作用域的隐藏方法 可以用函数来隐藏内部实现,把它包裹在函数内,外面就无法访问到了
- 好处一:
最小特权原则(最小授权、最小暴露原则)
- 好处二:避免同名标识符之间的冲突
应用
- 全局命名空间
- 模块管理
封装原始版本:
var a = 2;
function foo(){
var a = 3;
console.log( a);
}
foo();
console.log(a); //2
---不太理想,因为需要声明一个foo(),还需要显式的调用这个函数才能运行
改进版
var a =2;
(function foo(){
var a = 3;
console.log(a);
})(); //注意这一行
console.log(a);
以(function...而不是 function ...开始,函数会被当做函数表达式而不是一个标准的函数声明来处理,这是重要区别
区分函数声明和表达式最简单的方法是看function关键字出现在声明中的位置,如果function是声明的第一个词,那么就是一个函数声明----- 以! 、+ 、-、开头的都是这个逻辑,把函数声明变为了函数表达式
(function foo(){...})作为函数表达式意味着foo只能在(...)所代表的位置中被访问,外部不行
匿名和具名
- 函数表达式可以匿名,函数声明不可以
- 行内函数表达式
- 立即执行函数表达式 IIFE Immediately Invoked Function Expression
块作用域
- with 是一个块作用域的一种形式
- try catch语句中catch会创建一个块作用域
let关键字
let为其声明的变量隐式的指定了所在的块作用域
可以显式的用{...}创建块,使 其与其他语言中的块作用域工作原理一致
- let的声明不会在块作用域内进行提升,此代码运行之前,声明并不存在
{ console.log(bar); // ReferenceError!
let bar = 2;
}
- 与垃圾回收机制有关,便于清楚垃圾
let的应用 ----循环中
const关键字
也创建块作用域变量
第四章
1、 a =2; var a ; console.log (a); //2
2、console.log(a); var a = 2; //undefined
编译器
引擎会在解释js代码之前先对其进行编译----编译阶段的一个任务就是,找到所有的声明,并用合适的作用域将他们关联起来,因此包括变量和函数在内的所有声明都会在任何代码被执行之前首先被处理。
var a= 2也许看起来是一个声明,但是js会将其理解为2个声明,var a和a= 2,第一个在编译阶段进行,第二个会被留在原地等待执行阶段,这解释了第二个例子,a只声明了,没有被赋值,赋值操作在console语句之后进行
- 每个作用域都会进行提升操作
- 函数表达式不会被提升操作
- 函数会首先被提升,然后才是变量
- 普通块内部的函数声明会被提升至所在作用域的顶部,这个过程不会被暗示的条件判断控制
foo(); //"b"
var a = true;
if(a){
function foo(){ console.log("a") }
}else{
function foo() {console.log("b");}
}
第五章
闭包定义:
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行
闭包使得函数可以继续访问定义时的词法作用域
经典闭包题:
for (var i=1;i<=5;i++){
setTimeout( function timer(){
console.log(i);
},i*1000);
}
结果是以每秒一次的频率输出5次6
模块
一个从函数调用所返回的,只有**数据属性**而没有**闭包函数**的对象并不是真正的模块
模块模式的两个必要条件:
- 必须有外部的封闭函数,该函数必须至少被调用一次(每次调用都会创建一个新的模块实例)。
- 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有的状态。
模块模式的两个特点:
- 为函数定义引入包装函数(module.define(name,function(){}))
- 保证它的返回值和模块的API保持一致
ES6为模块增加了一级语法支持
浏览器对es6的支持,需要额外添加babel转换------下载babel后把babel链入文件,并把js文件类型改为type="text/babel",就可以了
附录A
js是基于词法作用域的语言,在bar内调用foo时,foo基于自己的作用域链进行查找
function foo(){
console.log(a); //////////2(不是3)
}
function bar(){
var a=3;
foo();
}
var a=2;
bar();
而this的运行机制是动态作用域
- 词法作用域关注函数在何处声明
- 动态作用域关注函数从何处调用
附录C
var foo=a=>{
console.log(a);
}
foo(2);///////2
- 胖箭头写法代表function
- 箭头函数在涉及this绑定时的行为和普通函数的行为完全不同,它放弃了所有普通this绑定的规则,取而代之的是用当前的词法作用域覆盖了this本来的值
第二部分第一章
在函数内部引用自身,可以用指向函数对象的词法标识符来引用,但是匿名函数没有词法标识,就不行了,另一种是arguments.callee来应用当前正在运行的函数对象,对匿名函数也有效。
this是什么
this是在运行时进行绑定的,它的上下文取决于函数调用时的各种条件
第二章
- 函数名称引用中会丢失this
- 隐式赋值中会丢失this
- 回调函数会丢失this
通过call()和apply()方法来显式的绑定this
判断this
- 是否在new中调用--
- 是否通过call,apply或者硬绑定
- 是否在某个上下文对象中调用(隐式绑定)
- 如果都不是,那么使用默认绑定
es6中可以用...操作符来代替apply(...)展开数组,foo(...[1,2])和foo(1.2)是一样的
`创建一个空对象比较安全 Object.create(null);
箭头 函数不会使用四条标准的绑定规则,和self=this机制一样,继承外层函数调用的this绑定
第三章 对象
定义:声明形式和构造形式
声明形式:
var myObj={
key:value
/ / ...
};
构造形式:
var myObj= new Object();
myObj.key=value;
简单基本类型:string boolean number null undefined ——————并不是对象
(null 执行typeof会返回object,是因为不同对象底层表示为二进制,二进制前三位都为0的话会被判断为object,null的二进制表示全是0,因此会被错误判断)
函数和数组都是对象的一种类型
但是有对象子类型,叫内置对象:
String Number Boolean Object Function Array Date RegExp Error
对象的属性:
属性名称与值,属性名称存在对象容器内部,但是值一般不存在对象内部,而是通过属性名称作为指针指向它的存储位置。
两种访问方式:myObject.a 或者 myObject[a]
前者为属性访问,后者为键访问
两者有些区别,属性访问要求要满足标识符的命名规范,因此Super-Fun!这种属性名不符要求,由于后者是使用字符串访问,因此可以在程序中构造这个字符串
ES6新增了可计算属性名
var prefix = "foo"
var myObject = {
[prefix + "bar" ] :"hello",
[prefix + "baz"] : "world"
};
对象的浅复制和深复制
es6的浅复制 Object.assign()
var newObj = Object.assign({},myObject);
浅复制值会复制旧对象的值,对象的话会引用自原对象
es5的属性描述符:
Object.getOwnPropertyDescriptor(myObject,"a")
获取myObject的a属性的描述
三个特性:writable/configurable/enumerable
可修改/可配置/可列举(比如是否出现在for --in中)
默认或者设置 Object.defineProperty(...)
- 禁止一个对象添加新属性并保留已有属性:
Object.preventExtensions(...) - 密封:Object.seal(...)可以修改现有属性但是不能添加新属性或重新配置或删除现有的seal()=preventExtensions+confugurable:false
- 冻结:Object.freeze(...)禁止对对象任意直接属性的修改 freeze=seal+writable:false
var myObject= {
a:2
};
myObject.a;
这个过程中实现了[[Get]]操作,先在对象中查找是否有名称相同的属性,如果没有,会继续遍历可能存在的原型链
还有一个[[Put]]
属性的getter和setter:成对出现
var myObject = {
get a(){
return this.a;
},
set a(val){
this.a=val*2;
};
myObject.a = 2;
myObject.a; /////4
一个属性undefined时,判断是属性中存着undefined还是属性本身不存在
var myObject = {
a:2
};
("a" in myObject);/ /true
("b" in myObject);/ /false
或者
myObject.hasOwnProperty("a");/ /true
myObject.hasOwnProperty("b");/ /false
二者略有不同,in 操作符会检查对象自身及其原型链(检查是否有这个属性名,而不是值,这个对于数组来说区别明显 4 in[2,4,6]返回false),而hasOwnProperty显然只检查对象自身(如果对象没有连接到Object.prototype这种方法会失败,因此改进一下,Object.prototype.hasOwnProperty.call(myObject,"a")就可以了)
枚举:指出现在对象的属性遍历中:
for (var k in myObject){
console.log(k,myObject[k]);
}
(数组的遍历用传统的for循环更好)
myObject.propertyIsEnumerable("a");/ /true
myObject.propertyIsEnumerable("b");/ /false
或者
Object.keys(myObject);会返回所有可枚举属性
Object.getOwnPropertyNames(myObject);会返回所有属性
es6的for of来遍历属性的值:
for(var v of myArray){
}
用内置的@@iterator迭代器来手动遍历数组:
var myArray = [1,2,3];
var it = myArraySymbol.iterator;
it.next();/ / {value:1, done:false}
it.next();/ / {value:2, done:false}
it.next();/ / {value:3, done:false}
it.next();/ / { done:true}
@@iterator本身不是一个迭代器对象,而是一个返回迭代器对象的函数,因此后边需要跟一个()
手动为一个对象添加自己的迭代器
var myObject = {
a:2,
b:3
};
Object.defineProperty(myObject,Symbol.iterator,{
enumerable:false,
writable:false,
configurable:true,
value:function(){
var o = this;
var idx = 0;
var ks = Object.keys(o);
return{
next:function(){
return{
value:o[ks[idx++]],
done:(idx>ks.length)
}
}
}
}
})
第四章
js中的显式混入对象:
function mixin(souceObj,targeObj){
for(var key in sourceObj){
if(!(key in targetObj)){
targetObj[key] = sourceObj[key];
}
}
js中不存在类,都是对象
函数多态:显式多态和相对多态
显式多态缺点多,频繁引用原函数
第五章 原型
myObject.foo = "bar"
会发生的事情:
- 如果myObject中已包含名为foo的普通数据访问属性,那么这条只会修改已有的属性值
- 如果遍历原型链也没找到foo,那么会在myObject上添加foo
- 然而如果原型链上层有这条属性,会出现3种情况:
A:如果上层存在普通数据访问属性,并且没有被标记为只读(writable:false),那么会直接在myObject中添加一个叫foo的新属性
B:如果上层有且为只读,那么会抛出错误或者被忽略
C:如果上层叫foo的是一个setter,那么会调用上层的setter
要注意避免隐式遮蔽:myObject.a++会隐式创建myObject.a
原型继承:委托机制
foo foo.prototype foo.prototype.constructor
constructor是默认构造foo时会添加给foo.prototype的属性,如果人为构造原型可能会丢失constructor
此外,var a=new foo();,a.constructor=foo并不代表,a是有constructor这个属性,该属性其实是[[Get]]查询原型链从foo.prototype那里引用来的
b.prototype=Object.create(a.prototype)
会创建一个新对象并把新对象内部的[[Prototype]]
关联到指定的对象
es6可以设定原型了:
Object.setPrototypeOf(Bar.prototype,Foo.prototype)
b.isPrototypeOf(a)——表达:b是否出现在a的原型链中
直接获取一个对象的[[Prototype]]链
Object.getPrototypeOf(a);
也有部分支持属性法:
a._proto_===Foo.prototype;
第六章
[[Prototype]]机制把对象关联到其他对象:
不同于类设计模式,方法名尽量不使用通用方法名,而是创建更有描述性的名字
注意区别类模式中的显式伪多态调用方法与委托模式中的委托调用的区别