第一章 作用域是什么
1.1编译原理
1.传统编译语言:
a.分词/词法分析:将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元。例:var a = 2;====>var、a、=、2;空格是否会被当作词法单元,取决于空格在这门语言是否有意义。
b.解析/语法分析:将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树(抽象语法树AST) a --- var --- = --- 2;
c.代码生成:将AST转换为可执行的代码的过程。
JS更加复杂,大部分情况下编译发生在代码执行前几微秒(甚至更短)的时间内。
1.2理解作用域
三个部分:
a.引擎:从头到尾负责整个JS程序的编译以及执行过程。
b.编译器:负责语法分析以及代码生成等。
c.作用域:负责收集并维护有所有声明的标识符(变量)组成的一系列查询,并实施一套严格的规则,确定当前执行的代码对这些标识符的访问权限。
对于var = 2来说,引擎认为这里有两个完全不同的声明,一个由编译器在编译时处理,另一个则由引擎在运行时处理。==》变量的赋值会有两个操作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能找到就会对它赋值。
LHS和RHS:
LHS和RHS的含义是“赋值操作的左侧和右侧"并不一定意味着就是”=赋值操作符的左侧和右侧“赋值操作还有其他几种形式,因此在概念是最好将其理解为"赋值操作的目标是谁(LHS)"和"谁是赋值操作的源头(RHS)"。
例:
function foo(a){
var b = a;
return a+b;
}
var c = foo(2)
这段代码中:
LHS:c = ...;、a = 2、b=...
RHS:foo(2..、= a、a..、..b
1.3 作用域嵌套
当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用域(也就是全局作用域为止)。
1.4 异常
为什么区分LHS和RHS是一件重要的事?
因为在变量还没有声明(在任何作用于都无法找到该变量)的情况下,这种查询的行为是不一样的。
如果RHS查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出ReferenceError异常。相较之下,当引擎执行LHS查询时,如果在顶层(全局作用域)也无法找到变量,全局作用域中会创建一个具有该名称的变量,并将其返回给引擎,前提是程序在非''严格模式''下。严格模式下LHS查询若没有查询到,同样会抛出ReferenceError异常。
第二章 词法作用域
2.1 词法阶段
词法作用域就是定义在此法阶段的作用域。换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里决定的,因此当词法分析器处理代码时会保持作用域不变(大部分是这样)。
PS:当全局变量被局部同名变量遮蔽可以用window.变量名调用。
2.2 欺骗词法
1.eval
js中的eval(...)可以接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中的这个位置的代码。换句话说,你可以在你写的代码中用程序生成代码并运行,就好像代码是写在那个位置一样。
在执行eval(...)之后的代码时,引擎并不"知道"或"在意"前面的代码是以动态形式插入进来,并对词法作用域的环境修改的。引擎只会如往常地进行词法作用域查找。
function foo(str,a){
eval(str);//欺骗
console.log(a,b);
}
var b = 2;
foo("var b = 3",1);//1,3
在严格模式中,eval在运行时有自己的词法作用域,意味着其中的声明无法修改所在的作用域。
2.with
with通常被当作重复引用同一个对象中的多个属性的快捷方式,可以不需以重复引用对象本身。如:
var obj ={
a:1,
b:2,
c:3,
};
//单调乏味的重复"obj"
obj.a = 2;
obj.b = 3;
obj.c = 4;
//简单的快捷方式
with(obj){
a=3;
b=4;
c=5;
}
严格模式下with被完全禁止。
eval和with使引擎无法在词法分析阶段明确知道eval会接受什么字段代码,这些代码会如何进行修改,也无法知道传递给with用来创建新词法作用域的对象的内容到底是什么。使得性能优化可能没有意义,副作用很大,一般不使用。
第三章 提升
包含变量和函数在内的所有声明都会在任何代码被执行前首先被处理。
当你看到var=2;时,可能认为这是一个声明。但JS实际上会将其看成两个声明:var a;和a = 2;.第一个声明是在编译阶段进行的,第二个赋值声明会被留在原地等待执行阶段。因此,打个比方,这个过程就好像比那辆和函数声明从他们在代码中出现的位置被"移动"到了最上面,这个过程叫做提升。
ps:
a.每个作用域都会进行提升操作。
b.函数声明会被提升,但函数作用域不会。
foo();//不是ReferenceError 而是TypeError!
var foo = function bar(){
//...
};
c.函数声明提升的优先级高于变量提升的优先级。
第四章 作用域闭包
3.1定义
当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。
function foo(){
var a = 2;
function bar (){
consloe.log(a);
}
return bar;
}
var baz = foo();
baz();//2
内部作用域依然存在 ,没有被销毁,就是因为bar()拥有覆盖foo()内部作用域的闭包,使得该作用域能够一直存在,以供bar()在之后任何时间进行引用。bar()依然持有对该作用域的引用,这个引用就叫闭包。
在定时器,事件监听器,Ajax请求,跨窗口通信,web workers或者任何其他的异步(或者同步)任务中,只要使用了回调函数,实际上就是在使用闭包。
ps:IFEE严格来讲并不是闭包,因为函数并不是在他本身的词法作用域以外执行的。
3.2循环和闭包
for(var i=1;i<=5;i++){
setTimeout(function timer(){
console.log(i);
},1000);
}
实际输出结果: 输出五次6。
原因:五个函数被封闭在一个共享的全局作用域中,因此实际上只有一个i。
解决:闭包
1.IIFE
for(var i=1;i<=5;i++){
(function(){
setTimeout(function timer(){
console.log(j);
},1000);
})(i);
}
2.let
for(let i=1;i<=5;i++){
function(){
setTimeout(function timer(){
console.log(i);
},1000);
}
}
3.3模块
为什么会出现模块呢?
如果所有的变量都是在全局作用域来保存,那么就会出现作用域污染,这个时候就引入了模块的概念。封装和最小暴露,将具有共同目的的代码段封装在一起,整合到一个单一的程序逻辑单元中,我们可以称他为组件,也可以叫一个模块。
现代模块与未来模块:
随着模板概念的不断演进,开发者发现可以使用IIFE立即执行函数,返回一个对象,我们可以将上面提到的共同目的的代码片段都封装在这个函数中,这就是基于IIFE的模块版本。
未来模块,也就是ES6的ESmodule。因为现在浏览器已经支持了import,export语法。我们可以使用export将一个模块导出,在另一个文件中用import导入。但是针对低版本浏览器就会降级到IIFE的这种。
第五章 this的全面解析
5.1调用位置
调用位置:函数被调用的位置
调用栈:为了到达当前执行位置所调用的所有函数
5.2绑定规则
1.默认绑定:
function foo(){
console.log(this.a);
}
var a=2;
foo();
在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的。
PS:如果使用严格模式,则不能将全局对象用于默认绑定,因此this会绑定到Undefined.
声明在非严格模式部分,之后在严格模式调用时,依旧可以默认绑定。
2.隐式绑定:
function foo (){
conslole.log(this.a);
}
var obj1 = {
a:2,
foo:foo
};
var obj2 = {
a:42
foo:foo
}
obj1.obj2.foo();//2
考虑调用位置上下文
隐式丢失
一个最常见的this绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定。
function foo(){
console.log(this.a);
}
function daFoo(fn){
//fn其实引用的是foo
fn();//<--调用位置
}
var obj = {
a:2,
foo:foo
};
var a = "这是global";//a是全局对象的属性
doFoo(obj.foo);//"这是global"
虽然bar是obj.foo的一个引用,但是实际上,他引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
5.3 显示绑定
隐式绑定我们必须在一个对象内部包含一个指向函数的属性,并通过这个属性间接引用函数,从而把this间接(隐式)绑定到这个对象上,那么如果我们不想再对象内部包含函数引用,二项在某个对象上强制调用函数,该怎么做呢?
a.call
b.apply
c.bind(硬绑定)
他们的第一个参数是一个对象,是给this准备的,接着再调用函数时将其绑定到this。因为你可以直接指定this的绑定对象,因此我们称之为显性绑定。
5.4 new绑定
1.构造一个全新的对象
2.这个新对象会被执行[[Prototype]]连接。
3.这个新对象会绑定到函数调用的this。
4.如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo (){
this.a = a;
}
var bar = new foo(2);
console.log(bar.a);//2
5.5 优先级
1.函数是否在NEW中调用?如果是的话this绑定的是新创建的对象。
2.函数是否通过call,apply(显式)或者硬绑定调用?如果是的话,this绑定的是指定的对象。
3.函数是否在某个上下文对象中调用(隐式)?如果是的话,this绑定的是哪个上下文对象。
4.都不是则使用默认绑定。
5.6 例外绑定
优先级之下凡事总有例外。
1.如果你把null,undefined作为this的绑定对象传入call,apply,或者bind,这些值在调用时会被忽略,实际使用的是默认绑定。
2.DMZ对象不会有null的副作用
间接引用
function foo()
{
console.log(this.a);
}
var a = 2;
var o = {a:3,foo:foo};
var p = {a:4};
o.foo();//3
(p.foo = o.foo)();//2
赋值表达式p.fpp = o.foo 的返回值是目标函数的音乐,因此调用位置是foo()而不是p/o.foo(),所以使用默认绑定。
软绑定
硬绑定会大大降低函数的灵活性,使用硬绑定之后就无法使用隐式绑定或者显式绑定来修改this指向。
软绑定允许函数在调用时:
1.首先检查是否有显式的this绑定
2.如果没有,则回退到预设的默认绑定
3.如果默认绑定也不存在,则回退到全局对象或undefined(严格模式)。
箭头函数
箭头函数不使用this的四种标准规则,而是根据外层(函数或者全局)作用于来决定this
第六章 对象
可以通过两种形式定义:
a.声明形式
var obj ={
key:value
};
b.构造形式
var myObj = new Object();
myObj.key = value;
PS:不同的对象在底层都表示为二进制,在JS中二进制前三位都为0的话会被判断为object类型,null的二进制表示全为0,自然前三位也是0,所以执行typeof时会返回“object”
内置方法
tostring,.length,chartAt,toFixed(参数为小数点后位数)
6.1 内容
对象的内容是有一些存储在特定命名位置的(任意类型的)值组成的,我们称之为属性。
这些值一般并不会存在对象容器内部。存储在对象容器内部的是这些属性的名称,他们就像指针(引用)一样,指向这些值真正的存储位置。
var obj = {
a:2
};
obj.a;//2 属性访问
obj["a"];//2 键访问
ps:在对象中,属性名永远是字符串,如果你使用String以外的其他值作为属性名,那它首先会被转化为一个字符串,即使是数字也不例外。
ES6增加了可计算属性名,可以在文字形式中使用[]包裹一个表达式当作属性名。
var a = "aa";
var b = {
[a+"c"]:"ac",
};
b["ac"];//ac
方法
1.函数在JS中本质上是独立的
2.函数引用是共享的,而非独占的
3.this的动态绑定是运行时行为,不影响函数的独立性。
数组
1.数组本质上也是对象,用于数值索引(非负整数下标)的数据存储
2.可以像普通对象一样添加命名属性
3.数字字符串属性会被强制转为数值索引:a["3"]===a[3]
复制对象
浅复制:
var new = Object.assign({},my);
var new = {...my};//使用扩展运算符
深复制:
递归复制所有的嵌套对象。
PS:问题:循环引用导致栈溢出或死循环。
JSON方法(仅限JSON安全对象):
var newObj = JSON.parse(JSON.stringify(someObj))
属性描述符
1.Writalble:是否可以修改
2.Configurable:是否是可配置的(false时 Writable:true可以改为false但不能改回去)
false也表示禁止删除这个属性
3.Enumerable:是否是可枚举的
不变性
有时候你会希望属性或者对象是不可改变的,
1.对象常量:结合Writable和congigurable:false就可以创建一个真正的常量属性(不可修改,删除,重定义)
var myObject = {};
Object,defineProperty(myObject,"num",{
value:2,
writable:false,
configurable:false
});
2.禁止扩展:如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(...)
var myObj = {
a = 2,
};
object.preventExtensions(myObj);
myObj.b = 3;
myObj.b;//undefined
在非严格模式下,创建属性B会静默失败,在严格模式下,抛出TypeError错误。
3.密封:object.seal(...)会创建一个”密封“的对象,这个方法实际上会在一个现有对象上调用,Object.preventExtensions(...),并把所有现有属性标记为configurable:false。
不仅不能添加,也不能重新配置或者删除(可以修改)。
4.冻结:object.freeze(...)会创建一个冻结对象,这个方法实际上会在一个现有的对象上调用Object.seal(...)并把所有数据访问属性表记为writable:false。
[[Get]]
对象默认内置[[get]]操作首先在对象中查找是否有名称相同的属性,如果找到就会返回这个属性的值。然而,如果没有找到名称相同的属性,按照[[Get]]算法的定义会执行另外一种非常重要得行为(原型链)。如果无论如何都没有找到名称相同的属性就返回undefined。和访问变量不一样,如果你引用了一个当前词法作用域不存在的变量,会抛出ReferenceError.
[[Put]]
如果对象已经存在这个属性:
1.属性是否是访问描述符
2.属性的数据描述符中writable是否是false?如果是,在非严格模式下静默失败,严格模式下抛出TypeError异常。
3.如果都不是,将该值设置为属性的值。
Getter和Setter
1.他们是隐藏函数,分别控制属性的获取和设置操作。
Getter在获取属性值是自动调用。
Setter在设置属性值是自动调用。
2.访问描述符:当属性定义了getter或setter时,他被称为“访问描述符”。
JS会忽略访问描述符的value和writable等特性。
转而关注get set configurable和enumerable特性。
ps:如果只定义getter而不设置setter,尝试设置属性值会被忽略(严格模式下会报错),应成对出现。
存在性
1."a" in myObj//检查属性是否在其对象及其原型链中
2.myObect.hasOwnProperty("a");//只检查属性是否在其对象中
数组用枚举:
for in 可枚举:可以出现在对象属性的遍历中。
ps:最好只在对象上应用for in循环。
检测枚举型:myObject.propertyIsEnumerable("a");
返回自身可枚举属性的数组:Objct.keys
返回自身所有属性的数组:Object.getOwnPropertyNames.
遍历
for in 便利的是可枚举属性,若想遍历属性的值--》
forEach,every,some ,for of(循环每次调用myObject迭代器对象的next()方法时,内部的指针都会向前移动并返回对象属性列表的下一个值)
当执行for of时,首先调用对象的[Symbol.iterator]方法获取迭代器对象,然后重复调用迭代器对象的next()方法,每次调用next()返回一个包含value和done属性的对象,当done为true时停止迭代。
第七章 原型
7.1 [[Prototype]]
js中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。(可以赋空,但很少见)
[[Prototype]]有什么用呢? :当你试图引用对象的属性是会触发[[Get]]操作。对于默认的,第一步是检查对象本身是否有这个属性,如果有的话就用它。但是如果a不在,就需要用对象的[[Prototype]]链了。
for in遍历对象时候和原型链类似。
ps:
哪里是[[Prototype]]的尽头呢?
所有普通的原型链最终都会指向内置的Object.prototype。
7.2 属性设置和屏蔽
一段代码 mObject.foo = "bar".
- 如果对象中包含名为foo的普通数据访问属性,这条赋值语句只会修改已有的属性值。
- 2.如果foo不存在于该对象中,原型链就会被遍历,类似Get操作。如果原型链上找不到foo,foo就会被直接添加到mObject中。
- 如果foo即出现在mObject中,又出现在原型链上层,那么会发生屏蔽。mObject中包含的foo属性会屏蔽原型链上层的所有foo属性,因为总是会选择原型链中最底层的foo属性
- 如果foo不存在于mObject中而直接存在于原型链上层,会有三种情况 a.如果在原型链上存在名为foo的普通数据访问属性,并且没有标记为只读,那就会直接在mObject中添加一个名为foo的屏蔽属性。 b.如果被标记为只读,则没办法,严格模式下会报错。 c.如果在原型链上层存在一个foo并且它是一个setter,那就一定会调用这个setter。foo不会被添加到(或者说屏蔽于)mObject,也不会重新定义foo这个setter。
如果想在第二第三种情况下也屏蔽foo,那就不能使用 = 赋值,要使用object.defineproperty(...)来向myObject中添加foo.
7.3 类
1.JS语言中没有真正的类,通过对象直接定义自身的行为,为非通过类复制行为。
2.new是对普通函数进行一个劫持。并用构造对象的形式来调用它,new Foo()并未复制‘类’的行为,只是常见了一个新对象,并将其原型链连接到Foo.prototype(Foo的原型对象)上。对象之间通过原项链关联,而非实例化是复制属性/方法。
3.Foo.prototype.constructor====Foo是默认属性,不代表对象由Foo"构造"。(仅是委托链的引用)
4.constructor属性的不可靠性:
function Foo(){/*...*/}
Foo.prototype = {/*...*/}
var a1 = new Foo();
a1.constructor === Foo;//false
a1.constructor === Object;//true
修改Foo.prototype会破坏constructor。consturctor不表示对象被构造的来源,而是被覆盖的普通属性。
7.4 (原型)继承
function Foo(name){
this.name = name;
}
Foo.prototype.myname = function(){
return this.name;
}
function Bar(name,level){
Foo.call(this,name);
this.level = level;
}
Bar.prototype = Object.create(Foo.prototype);
Bar.prototype.mylevel = function(){
return this.level;
}
var a = new Bar("a",2);
a.myname();//a
a.mylevel();//2
两种错误想法:
1.Bar.prototype = Foo.prototype;
只是引用Foo.prototype对象。因此当你执行类似Bar.prototype.mylevel= ...的赋值语句时会直接修改Foo.prototype对象本身。
2.Bar.prototype = new Foo();
ES6新关联方法:Object.setPrototypeOf(Bar.prototype,Foo.prototype);//不需要抛弃默认的Bar.prototype。
7.5 _proto_
检查一个实例的继承祖先:
1.Foo.prototype.isPrototypeOf(a)
2.可以直接获取一个对象的[[prototype]]链。
ES5中:Object.getPrototypeOf(a); Object.getPrototypeOf(a)===Foo.prototype;
ES6:_proto_: a._proto_ ===Foo.prototype
_proto_实现:
Object.defineproperty(Object.prototype,"_proto_",{
get:function(){
return Object.getprototypeOf(this);
}
set:function(o){
Object.setPrototpeOf(this,o);
return o;
}
})
原型链定义:
如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找他的[[Prototype]],以此类推。这一系列对象的链接叫做原型链。
object.create第二个参数指定了需要添加到新对象中的属性名以及这些属性的属性描述符。
7.6 关联关系是否备用
var aObject = {
cool:function(){
console.log("cool");
}
}
var myObject = Object.create(aObject);
myObject.doCool = function(){
this.cool;//内部委托
}
myObject.doCool();//"cool"
这里我们调用的myObject.doCool是实际存在于myObject中的,这可以让我们的API设计更加清晰。
2862

被折叠的 条评论
为什么被折叠?



