1.问:为何要写这么长,有必要吗?是否是脑子秀逗了?
答:我想这是大部分人看到这个标题都会问的问题.由于做为一个男人,我喜欢长一点,也不喜欢分割成几个部分.一家人就要在一块儿,整整齐齐.好吧,正经点,其实整篇前言能够说都是在回答这个问题.你能够选择先看完前言,再决定要不要和书本搭配起来阅读. 这里先简单捋一下:1,内容多:首先这篇读书笔记原本内容就不少,是对书本的全方位详解.2,针对新人:针对那种红宝书草草读过一遍,对js只浮于接口调用的新手.3,留给读者本身提炼:读这种社科类书籍通常是先读厚,再读薄.这篇笔记就属于最开始'读厚'的阶段.在读者完全读懂后,再本身进一步提炼.关于怎么读书,我后面会详细介绍.前端
2.问:这么长,那到底包含了些什么内容?
答:笔记的目录结构和书本的彻底一致.对每一节的内容进行更通俗的解读(针对新人),对示例进行更深的说明,有的会辅以流程图,并提供对应的mdn链接;对内容进行概括,小节脉络更清晰;添加了大量实际工做时的注意事项,增长了更加清晰和易懂的示例及注释,并在原文基础上进行了拓展和总结;对书中的错误和说了后面会进行介绍
,而没有介绍的填坑,翻译或者容易引发误会的称呼的说明;添加了我的读书时的感觉和吐槽.vue
3.问:书已经够多了,还要看你这么长的笔记?
答:首先你要知道读这种技术类书籍,不是读小说!读完并不意味着你读懂了.而是须要将书中的知识转换成你本身的.这篇笔记就是,帮助新手更方便地理解知识点,更流畅地进行阅读.也能够在读完一节后,经过对比,发现本身有什么知识点是不懂或者遗漏,理解有误的. 而且一些注意事项,容易被误导的,关于书中观点的吐槽等等,其实想说的都已经写在笔记里了.node
4.问:这本书到底怎么样,有没有其余人说的那么好?
答:这是一个先扬后抑的回答.首先毫无疑问这是一本很是不错的书!它系统地全面地对JavaScript进行解读,优势缺点全都有.当你完全读懂这本书后,你对JavaScript的几乎全部疑问都会获得解答(我对做用域是否是"对象"的疑问?也获得了解答).但它也是有必定门槛的,若是你对JS不熟,经常使用接口都不熟,不少名词的表层意思都不太理解.这本书并不适合你,你花在问谷歌娘的时间可能比你读书的都长,读起来也是只知其一;不知其二;不一样于其余书,这本书不少时候没有给出明确的概念定义,须要你本身反复阅读理解他的话.每一小节的脉络结构也不是那么清晰,有时候须要本身去梳理;不知道是否是翻译的锅,不少东西解释得有点迷,原本很简单,但却说一堆并不经常使用的术语(可能国内不是这么叫的),看得你一脸懵逼!有时候同一个概念,先后会出现三四个不一样的名词进行指代,没有任何说明;整本书,具备很强的做者主观情感在里面.前半段,把JS捧得很高,说它引擎的各类优化好!但到后半段关于JavaScript中模拟类和继承"的批评,说它们具备很大误导性!更是嗤之以鼻!就差爆粗口了,好像JavaScript就是一个异教徒,应该绑在十字架上被烧死!可是他这样的观点,都是站在其余类语言的角度来看待,产生的.我想更多的读者多是只接触过JavaScript这一种语言,对他们来讲,实际上是根本没有这些"疑惑"的!jquery
1.不要抱任何功利和浮躁的心来读书!
这种以理论,概念为主的书,其实你们都是不那么愿意读的.一是读起来很费劲,抽象.二是实际工做,几乎不会用到,在如今浮躁的前端圈这是吃力不讨好.那这本书最大的用处是什么?没错,就是被不少人用来应付面试!? 这自己没什么问题,你读懂系列三本书,全部涉及JS的面试都能轻松应对.可是当抱着功利心时,你更多的则是敷衍.对书中的概念进行机械的复制,再粘贴上本身肤浅的理解.OK,应付那些也是跟风的面试官足够了.通常你回答了,他们也不会继续往下问,问深了本身也不清楚,也很差否认你.若是你够自信,'瞎扯'也能够唬住.若是你答不上,脸皮厚的会让你回去本身查.真正知道的面试官,其实都是会给你解释的,他们也不会忙到差这点时间.其实他们内心也是很乐意展现本身学识丰富的一面.
这种功利读书方式,即便你读完了(更多人是半途而废),对你的技术也不会有任何帮助.由于读完,你实际上是只知其一;不知其二的.这样反而更糟,甚至可能会对你以前JavaScript正确的理解产生混淆.webpack
2.认认真真读完一本书好过收藏一百篇相关文章(其实你压根连一半都不会看)!ios
我一直认为想系统弄懂一门知识,书本才是最好的选择,它绝对比你东拼西凑找来的一堆文章要好得多!如今前端圈随便看看,一大堆全是原型链,闭包,this...这些内容.里面的内容大同小异,不少理解也是比较浅显,考虑的也比较片面.但浮躁的人就是喜欢这种文章,以为本身收藏了,看了就完全理解了(!?).其实这些文章里有不少都是借鉴了本书.程序员
首先,你必须知道知识都是有体系的,不是彻底独立的.例如想要完全理解,原型链,闭包,this.就必须先弄清做用域和函数.知识都是环环相扣,相互关联的.若是你想完全弄懂,仍是选择读书吧,由浅入深,全面理清全部知识点的关联.记住 "只知其一;不知其二"永远比"无知"更糟!(固然不懂装懂,还振振有词的人另当别论).es6
传统编译的三个步骤web
说明: 此处只需记住第一步:分词/词法分析.第二步:解析/语法分析,获得抽象语法树(AST).第三步:代码生成,将抽象语法树转换为机器指令.面试
JavaScript与传统编译的不一样点:
首先介绍将要参与到对程序 var a = 2; 进行处理的过程当中的演员们,这样才能理解接下来将要听到的对话。
由上图可知,引擎在得到编译器给的代码后,还会对做用域进行询问变量.
如今将例子改成var a = b;此时引擎会对变量a和变量b都向做用域进行查询.查询分为两种:LHS和RHS.其中L表明左.R表明右.即对变量a进行LHS查询.对变量b进行RHS查询.
单单从表象上看.LHS就是做用域对=
左边变量的查询.RHS就是做用域对=
右边变量的查询.但实际上并非这么简单,首先LHS和RHS都是对变量进行查询,这也是我为何要将例子从var a=2;改成var a=b;二者的区别是二者最终要查询到的东西并不一致.LHS是要查询到变量的声明(而不是变量的值),从然后面能够为其赋值.RHS是要查询到变量最终的值.还有一点,LHS 和 RHS 的含义是“赋值操做的左侧或右侧”并不必定意味着就是“= 赋值操做符的左侧或右侧”。赋值操做还有其余几种形式,所以在概念上最 好将其理解为“赋值操做的目标是谁(LHS)”以及“谁是赋值操做的源头(RHS)”.或者这样理解若是这段代码须要获得该变量的'源值',则会进行RHS查询.
这部分比较简单就是经过拟人方式比喻引擎和做用域的合做过程.一句话归纳就是,引擎进行LHS和RHS查询时都会找做用域要.
function foo(a) { console.log( a ); // 2 } foo( 2 ); 复制代码
让咱们把上面这段代码的处理过程想象成一段对话,这段对话多是下面这样的。
引擎:我说做用域,我须要为 foo 进行 RHS 引用。你见过它吗?
做用域:别说,我还真见过,编译器那小子刚刚声明了它。它是一个函数,给你。
引擎:哥们太够意思了!好吧,我来执行一下 foo。
引擎:做用域,还有个事儿。我须要为 a 进行 LHS 引用,这个你见过吗?
做用域:这个也见过,编译器最近把它声名为 foo 的一个形式参数了,拿去吧。
引擎:大恩不言谢,你老是这么棒。如今我要把 2 赋值给 a。
引擎:哥们,很差意思又来打扰你。我要为 console 进行 RHS 引用,你见过它吗?
做用域:咱俩谁跟谁啊,再说我就是干这个。这个我也有,console 是个内置对象。 给你。
引擎:么么哒。我得看看这里面是否是有 log(..)。太好了,找到了,是一个函数。
引擎:哥们,能帮我再找一下对 a 的 RHS 引用吗?虽然我记得它,但想再确认一次。
做用域:放心吧,这个变量没有变更过,拿走,不谢。
引擎:真棒。我来把 a 的值,也就是 2,传递进 log(..)。
当一个块或函数嵌套在另外一个块或函数中时,就发生了做用域的嵌套。进而造成了一条做用域链.所以,在当前做用 域中没法找到某个变量时,引擎就会在外层嵌套的做用域中继续查找,直到找到该变量, 或抵达最外层的做用域(也就是全局做用域)为止。
当引擎须要对做用域进行查询时.引擎会从当前的执行做用域开始查找变量,若是找不到, 就向上一级继续查找。当抵达最外层的全局做用域时,不管找到仍是没找到,查找过程都 会中止。
例子:
function foo(a) { console.log( a + b ); b = a; } foo( 2 ); 复制代码
console.log(a+b)
因为RHS此时是找不到b的值.故会抛出ReferenceError.b=a;
.=
操做符或调用函数时传入参数的操做都会致使关联做用域的赋值操做。此时都会进行LHS查询做用域分为两种工做模式:
词法阶段: 大部分标准语言编译器的第一个工做阶段叫做词法化(也叫单词化)。词法化的过程会对源代码中的字符进行检查,若是是有状态的解析过程,还会赋予单词语义。
词法做用域: 词法做用域就是定义在词法阶段的做用域也被称为静态做用域。即在JavaScript里做用域的产生是在编译器出来的第一阶段词法阶段产生的,而且是你在书写完代码时就已经肯定了的.
词法做用域位置: 词法做用域位置范围彻底由写代码期间函数所声明的位置来决定.
理解词法做用域及嵌套: 看下例子:
function foo(a) { var b = a * 2; function bar(c) { console.log( a, b, c ); } bar( b * 3 ); } foo( 2 ); // 2, 4, 12 复制代码
在这个例子中有三个逐级嵌套的做用域。为了帮助理解,能够将它们分红3个逐级包含的"气泡做用域"。
注意: 没有任何函数的气泡能够(部分地)同时出如今两个外部做用域的气泡中,就如同没有任何函数能够部分地同时出如今两个父级函数中同样。
引擎对做用域的查找:
这一部分在上一节中已经说过,就是从当前做用域逐级向上,直到最上层的全局做用域.这里再进一步进行讲解.做用域查找会在找到第一个匹配的标识符时中止。在多层的嵌套做用域中能够定义同名的标识符,这叫做“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应, 做用域查找始终从运行时所处的最内部做用域开始,逐级向外或者说向上进行,直到碰见第一个匹配的标识符为止。
注意:
window.a
经过这种技术能够访问那些被同名变量所遮蔽的全局变量。但非全局的变量若是被遮蔽了,不管如何都没法被访问到。欺骗词法: 引擎在运行时来“修改”(也能够说欺骗)词法做用域.或者说就是在引擎运行时动态地修改词法做用域(原本在编译词法化就已经肯定的).
欺骗词法的两种机制:(下面这两种机制理解了解便可,不推荐实际开发使用)
JavaScript 中的 eval(..) 函数能够接受一个字符串为参数,并将其中的内容视为好像在书写时就存在于程序中这个位置的代码。即将eval放在该词法做用域,而后eval携带的代码就会动态加入到该词法做用域.
经过下面的例子加深理解:
function foo(str, a) { eval( str ); // 欺骗! console.log( a, b ); } var b = 2; foo( "var b = 3;", 1 ); // 1, 3 复制代码
eval(..) 调用中的 "var b = 3;" 这段代码会被看成原本就在那里同样来处理。因为那段代码声明了一个新的变量 b,所以它对已经存在的 foo(..) 的词法做用域进行了修改。当 console.log(..) 被执行时,会在 foo(..) 的内部同时找到 a 和 b,可是永远也没法找到外部的 b。所以会输出“1, 3”而不是正常状况下会输出的“1, 2”。
注意:
var sum = new Function("a", "b", "return a + b;"); console.log(sum(1, 1111)); //1112 复制代码
例子:
function foo(obj) { with (obj) { a = 2; } } var o1 = { a: 3 }; var o2 = { b: 3 }; foo( o1 ); console.log( o1.a ); // 2 foo( o2 ); console.log( o2.a ); // undefined console.log( a ); // 2——很差,a 被泄漏到全局做用域上了! 复制代码
起初你会以为o1的a属性被with里的a进行了词法引用被遮蔽了成为了2.而o2没有a属性,此时with不能进行词法引用,因此此时o2.a就会变成undefined.可是,为何最后console.log(a)会为2?由于在执行foo(o2)时,with会对其中的a=2进行LHS查询,但它在o2做用域,foo()做用域,全局做用域都没找到,所以就建立了一个全局变量a并随后赋值2.
总的来讲,with就是将一个没有或有多个属性的对象处理为一个彻底隔离的词法做用域,所以这个对象的属性也会被处理为定义在这个做用域中的词法标识符。
注意: 使用 eval(..) 和 with 的缘由是会被严格模式所影响(限制)。with 被彻底禁止,而在保留核心功能的前提下,间接或非安全地使用 eval(..) 也被禁止了。
JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于可以根据代码的词法进行静态分析,并预先肯定全部变量和函数的定义位置,才能在执行过程当中快速找到标识符。可是eval(..) 和 with会在运行时修改或建立新的做用域,以此来欺骗其余在书写时定义的词法做用域。这么作就会致使引擎没法知道eval和with它们对词法做用域进行什么样的改动.只能对部分不进行处理和优化!所以若是代码中大量使用 eval(..) 或 with,那么运行起来必定会变得很是慢!。
由于
因此用函数声明对代码进行包装,实际上就是把这些代码“隐藏”起来了。
为何要将代码进行"隐藏"? 由于最小受权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必 要内容,而将其余内容都“隐藏”起来,好比某个模块或对象的 API 设计。 隐藏的好处:
函数声明与函数表达式:
function foo() { ... } 复制代码
咱们知道函数foo内的变量和函数被隐藏起来了,是不会对全局做用域形成污染.可是变量名foo仍然存在于全局做用域中,会形成污染.那有什么方法能避免函数名的污染呢?那就是做为函数表达式,而不是一个标准的函数声明.这样函数名只存在于它本身的函数做用域内,而不会存在于其父做用域,这样就没有了污染.举个函数声明的例子:
var a = 2; (function foo(){ var a = 3; console.log( a ); // 3 })(); console.log( a ); // 2 复制代码
当咱们用()包裹一个函数,并当即执行.此时这个包装函数声明是从(function
开始的而不是从function关键字开始.这样foo就会被当作一个函数表达式,而不是一个函数声明(即foo不会存在于父级做用域中).回到上面的例子中,全局做用域是访问不到foo的,foo只存在于它本身的函数做用域中.
补充: 什么是函数声明和函数表达式 首先咱们得了解JS声明函数的三种方式:
!
,+
,-
,()
.举个例子:!function(){console.log(1)}()
的结果是1,并不会报错)//Function()构造器 var f =new Function() // 函数表达式 var f = function() { console.log(1); } // 函数声明 function f (){ console.log(2); } console.log(f()) //思考一下,这里会打印出什么 复制代码
怎么区分函数声明和函数表达式: 看 function 关键字出如今声明中的位置(不只仅是一行代码,而是整个声明中的位置)。若是 function 是声明中的第一个词,那么就是一个函数声明,不然就是一个函数表达式。例如上例中,是从(
开始而不是function.
补充: 上面这段是原书的解释,我以为这个解释并不彻底,这里给出我本身的解释.
var f = function fun(){console.log(fun)}
其余地方是没有的.这也避免了全局污染,也方便递归.函数表达式能够是匿名的,而函数声明则不能够省略函数名.有函数名的就是具名函数,没有函数名的就是匿名函数.
匿名函数的缺点:
因此给函数表达式指定一个函数名能够有效解决以上问题。始终给函数表达式命名是一个最佳实践.
PS: 我的意见是若是函数表达式有赋值给变量或属性名或者就是一次性调用的.实际上是不必加上函数名.由于代码里取名原本就很难,取很差反而会形成误解.
好比 (function foo(){ .. })()。第一个 ( ) 将函数变成表达式,第二个 ( ) 执行了这个函数。这就是当即执行函数表达式,也被称为IIFE,表明当即执行函数表达式 (Immediately Invoked Function Expression);
IIFE能够具名也能够匿名.好处和上面提到的同样.IIFE还能够是这种形式(function(){ .. }())
.这两种形式在功能上是一致的。
函数做用域是JavaScript最多见的做用域单元,有时咱们仅会将var赋值变量在if或for的{...}内使用,而不会在其余地方使用.但它仍然会对外层的函数做用域形成污染.这个时候就会但愿能有一个做用域能将其外部的函数做用域隔开,声明的变量仅在此做用域有效.块做用域(一般就是{...}包裹的内部)就能够帮咱们作到这点.
从 ES3 发布以来,JavaScript 中就有了块做用域,而 with 和 catch 分句就是块做用域的两个小例子。
咱们在第 2 章讨论过 with 关键字。它不只是一个难于理解的结构,同时也是块做用域的一个例子(块做用域的一种形式),用 with 从对象中建立出的做用域仅在 with 声明中而非外部做用域中有效。
try/catch 的 catch 分句会建立一个块做用域,其中声明的变量仅在 catch 内部有效。
try { undefined(); // 执行一个非法操做来强制制造一个异常 } catch (err) { console.log( err ); // 可以正常执行! } console.log( err ); // ReferenceError: err not found 复制代码
err 仅存在 catch 分句内部,当试图从别处引用它时会抛出错误。 那么若是咱们想用catch建立一个不是仅仅接收err的块做用域,该怎么作呢?
try{throw 2;}catch(a){ console.log( a ); // 2 } console.log( a ); // ReferenceError 复制代码
这样就建立了一个块做用域,且a=2,仅在catch分句中存在.在ES6以前咱们能够使用这种方法来使用块做用域.
ES6 引入了新的 let 关键字,提供了除 var 之外的另外一种变量声明方式。let 关键字能够将变量绑定到所在的任意做用域中(一般是 { .. } 内部)。
用 let 将变量附加在一个已经存在的块做用域上的行为是隐式的。例如在if的{...}内用let声明一个变量.那什么是显式地建立块做用域呢?就是单首创建{}
来做为let的块做用域.而不是借用if或者for提供的{}
.例如{let a=2;console.log(a)}
注意: 使用 let 进行的声明不会在块做用域中进行提高.
块做用域的好处:
function process(data){ // 在这里作点有趣的事情 } var someReallyBigData=function(){ //dosomeing } process(someReallyBigData); var btn=document.getElementById("my_button"); btn.addEventListener("click",function click(evt){ alert("button click"); //假如咱们在这里继续调用someReallyBigData就会造成闭包,致使不能垃圾回收(这段是书里没有,我加上方便理解的) },false); 复制代码
click 函数的点击回调并不须要 someReallyBigData 变量。理论上这意味着当 process(..) 执行后,在内存中占用大量空间的数据结构就能够被垃圾回收了。可是,因为 click 函数造成了一个覆盖整个做用域的闭包,JavaScript 引擎极有可能依然保存着这个结构(取决于具体实现)。 但显式使用块做用域可让引擎清楚地知道没有必要继续保存 someReallyBigData 了:
function process(data){ // 在这里作点有趣的事情 } // 在这个块中定义的内容能够销毁了! { let someReallyBigData = { .. }; process( someReallyBigData ); } var btn=document.getElementById("my_button"); btn.addEventListener("click",function click(evt){ alert("button click"); },false); 复制代码
for (let i=0; i<10; i++) { console.log( i ); } console.log( i ); // ReferenceError 复制代码
for 循环头部的 let 不只将 i 绑定到了 for 循环的块中,事实上它将其从新绑定到了循环的每个迭代中,确保使用上一个循环迭代结束时的值从新进行赋值。这样就避免了i对外部函数做用域的污染.
除了 let 之外,ES6 还引入了 const,一样能够用来建立块做用域变量,但其值是固定的(常量)。以后任何试图修改值的操做都会引发错误。
var foo = true; if (foo) { var a = 2; const b = 3; // 包含在 if 中的块做用域常量 a = 3; // 正常! b = 4; // 错误! } console.log( a ); // 3 console.log( b ); // ReferenceError! 复制代码
函数是 JavaScript 中最多见的做用域单元。本质上,声明在一个函数内部的变量或函数会在所处的做用域中“隐藏”起来,能够有效地与外部做用域隔开.
但函数不是惟一的做用域单元。块做用域指的是变量和函数不只能够属于所处的做用域,也能够属于某个代码块(一般指 { .. } 内部)即块做用域。ES6中就提供了let和const来帮助建立块做用域.
考虑第一段代码
a = 2; var a; console.log( a ); 复制代码
输出结果是2,而不是undefined
考虑第二段代码
console.log( a ); var a = 2; 复制代码
输出结果是undefined,而不是ReferenceError 考虑完以上代码,你应该会考虑这个问题.究竟是声明(蛋)在前,仍是赋值(鸡)在前?
编译器的内容,回忆一下,引擎会在解释 JavaScript 代码以前首先对其进行编译。编译阶段中的一部分工做就是找到全部的声明,并用合适的做用域将它们关联起来。 以后引擎会询问做用域,对声明进行赋值操做.
那么,在编译阶段找到全部的声明后,编译器又作了什么?答案就是提高 以上节的第一段代码为例,当你看到 var a = 2; 时,可能会认为这是一个声明。但 JavaScript 实际上会将其当作两个声明:var a;和a = 2;。 第一个定义声明是在编译阶段进行的。第二个赋值声明会被留在原地等待执行阶段。在第一个声明在编译阶段时,编译器会对var a;
声明进行提高(即把var a;
置于所在做用域的最上面).而a = 2;
则会保持所在位置不动.此时代码会变成
var a; a = 2; console.log( a ); 复制代码
由此可知,在编译阶段,编译器会对声明进行提高.即先有蛋(声明)后有鸡(赋值)。 哪些声明会被进行提高?
var a;
.不包括后面的a = 2;
即不包含有赋值操做的声明.函数声明和变量声明都会被提高。可是一个值得注意的细节(这个细节能够出如今有多个“重复”声明的代码中)是函数会首先被提高,而后才是变量。 考虑如下代码:
foo(); // 1 var foo; function foo() { console.log( 1 ); } foo = function() { console.log( 2 ); }; 复制代码
会输出 1 而不是 2 !这个代码片断会被引擎理解为以下形式:
function foo() { console.log( 1 ); } foo(); // 1 foo = function() { console.log( 2 ); }; 复制代码
注意,var foo 尽管出如今 function foo()... 的声明以前,但它是重复的声明(所以被忽略了),由于函数声明会被提高到普通变量以前。 注意: js会忽略前面已经声明的声明(无论是变量声明仍是函数声明,只要其名称相同,则后续不会再进行重复声明).可是对该变量新的赋值,会覆盖以前的值.
一句话归纳:函数声明的优先级高于变量声明,会排在它前面.
var a = 2
JavaScript引擎会将var a和 a = 2看成两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。由于这两小节理解透了其实发现书里也没讲什么,这里就进行合并,并补充拓展我本身的理解和总结.
什么是闭包?(广义版)
书中解释: 当函数能够记住并访问所在的词法做用域时,就产生了闭包,即便函数是在当前词法做用域以外执行。
MDN的解释: 闭包是函数和声明该函数的词法环境的组合。
个人解释(详细版): 必须包含两点:
综上简单版就是:MDN的解释闭包是函数和声明该函数的词法环境的组合。
还能够继续延伸成极简版:JavaScript中的函数就会造成闭包。
Tips: 注意到上面对词法做用域
和词法环境
两词的分开使用了吗?1,
里此时函数还没被执行,因此使用的是词法做用域即静态做用域.2,
里,此时函数被执行,此时词法做用域就会变成词法环境(包含静态做用域与动态做用域).因此其实MDN的解释其实更准确一点,
咱们平常使用时所说的闭包(狭义版,严格意义上的):
为了便于对闭包做用域的观察和使用.咱们实际使用时会将闭包的函数做用域暴露给当前词法做用域以外.也就是本书一直强调的闭包函数须要在它自己的词法做用域之外执行.做者认为符合这个条件才称得上是真正的闭包(也就是咱们平常使用常说的'使用闭包',而且使用任何回调函数其实也是闭包).
因此狭义版就是:闭包是函数和声明该函数的词法环境的组合,而且将闭包的函数做用域暴露给当前词法做用域以外.
闭包暴露函数做用域的三种方式:
下面部分是书中没有的,是本身实际使用时的总结,而且符合这三种形式之一的就是咱们平常使用时所说的闭包(狭义版)
function foo() { var a = 2; function bar() { baz(a) //经过外部函数的参数进行暴露 } bar(); }; function baz(val) { console.log( val ); // 2 } foo(); 复制代码
var val; function foo() { var a = 2; function bar() { val=a //经过外部做用域的变量进行暴露 } bar(); }; foo(); console.log(val) //2 复制代码
function foo() { var a = 2; function bar() { console.log(a) } return bar //经过return直接将整个函数进行暴露 }; var val=foo(); val() //2 复制代码
关于闭包的内存泄露问题:
首先必须声明一点:使用闭包并不必定会形成内存泄露,只有使用闭包不当才可能会形成内存泄露.(吐槽:面试不少新人时,张口就说闭包会形成内存泄露)
为何闭包可能会形成内存泄露呢?缘由就是上面提到的,由于它通常会暴露自身的做用域给外部使用.若是使用不当,就可能致使该内存一直被占用,没法被JS的垃圾回收机制回收.就形成了内存泄露.
注意: 即便闭包里面什么都没有,闭包仍然会隐式地引用它所在做用域里的所用变量. 正由于这个隐藏的特色,闭包常常会发生不易发现的内存泄漏问题.
常见哪些状况使用闭包会形成内存泄露:
function foo() { var a = {}; function bar() { console.log(a); }; a.fn = bar; return bar; }; 复制代码
这里建立了一个a 的对象,该对象被内部函数bar引用。而后,a建立了一个属性fn指向了bar,最后返回了innerFn()。这样就造成了bar和a的相互循环引用.可能有人说bar里不使用console.log(a)
不就没有引用了吗就不会形成内存泄露了.NONONO,bar做为一个闭包,即便它内部什么都没有,foo中的全部变量都仍是隐使地被 bar所引用。这个知识点是我前面忘记提到的,也是书中没有提到的.算了我如今加到前面去吧.因此即便bar内什么都没有仍是形成了循环引用,那真正的解决办法就是,不要将a.fn = bar
.
总而言之,解决办法就是使闭包的能正常引用,能被正常回收.若是实在不行,就是在使用完后,手动将变量赋值null,强行进行垃圾回收.
看以下例子:
for (var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); } 复制代码
咱们指望的结果是分别输出数字 1~5,每秒一次,每次一个。
但实际结果是,这段代码在运行时会以每秒一次的频率输出五次 6。
(关于书里的解释,我以为有点说复杂了,没说到点子上,下面是个人解释.)
为何会是这样的结果?
timer毫无疑问是一个闭包,它是能够访问到外部的变量i.在进行for循环时,timer()会被重复执行5次,也就是它会 console.log( i )5次.(关键部分来了!)这5次i
实际上是同一个i
.它是来自于外部做用域,即for里面声明的i.在词法做用域中变量i只可能对应一个惟一的值,即变量和它的值是一一对应的.不会变化的.那这个值究竟是多少呢?这个值就是最终值! i的最终值就是6即for循环完后i
的值.当引擎执行console.log( i )
时,它会询问i所对应的做用域,问它i的值是多少.这个时候做用域进行RHS查询获得的结果就是最终值6.
为何咱们会觉得分别输出1~5?
由于在for循环中,咱们错觉得每一次循环时,函数所输出的i是根据循环动态变化的.便是1~5累加变化的.但实际上它所访问的i是同一个固定不变的值,即最终值6.可能你会有这样的疑惑,那我循环还有意义吗?i其实一开始就肯定是6了.没有变化过!错!i
变化过,它的确是从1逐步增长到6的.只是外部做用域的i值只多是循环完后的最终值,而且函数timer()并无保存每次i变化的值.它只是访问了外部做用域的i值即最终的值6. OK咱们知道了出错的地方,就是咱们没有把每次i的值保存在一个独立的做用域中. 接下来,看下这个改进的例子结果是多少.
for (var i=1; i<=5; i++) { (function() { setTimeout( function timer() { console.log( i ); }, i*1000 ); })(); } 复制代码
它的最终值仍然是5个6.为何?咱们来分析下,上例中,它用了一个匿名函数包裹了定时器,并当即执行.在进行for循环时,会创造5个独立的函数做用域(由匿名函数建立的,由于它是闭包函数).可是这5个独立的函数做用域里的i也全都是对外部做用域的引用.即它们访问的都是i的最终值6.这并非咱们想要的,咱们要的是5个独立的做用域,而且每一个做用域都保存一个"当时"i
的值.
解决办法: 那咱们这样改写.
for (var i=1; i<=5; i++) { (function () { var j =i; setTimeout( function timer() { console.log( j ); }, j*1000 ); })(); } //此次终于结果是分别输出数字 1~5,每秒一次,每次一个。 复制代码
这样改写后,匿名函数每次都经过j保存了每次i值,这样i
值就经过j保存在了独立的做用域中.注意此时保存的i值是'当时'的值,并非循环完后的最终值.这样循环完后,实际上就建立了5个独立的做用域,每一个做用域都保存了一个'当时'i的值(经过j).当引擎执行console.log( j )
询问其对应的独立做用域时,获得的值就是'当时'保存的值,不再是6了. 咱们还能够进一步简写为这样:
for (var i=1; i<=5; i++) { (function(j) { setTimeout( function timer() { console.log( j ); }, j*1000 ); })(i); } //结果是分别输出数字 1~5,每秒一次,每次一个。 复制代码
利用块做用域进行解决:
在es6中,咱们不只能够使用函数来建立一个独立的做用域,咱们还能够使用let声明来建立一个独立的块做用域(在{}
内).因此咱们还能够这样改写:
for (let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); } //结果是分别输出数字 1~5,每秒一次,每次一个。 复制代码
这样改写,在每次循环时,let都会对i进行声明.并经过循环自带的{}
建立一个独立的块做用域.而且let声明的i,保存了'当时'i
的值在当前块做用域里.所以当引擎执行console.log( i )
时,它会询问对应的块做用域上i的值,获得的结果就是'当时'保存的值.
延伸:
实际上块做用域能够称得上一个'伪'闭包(之因此是伪,是由于闭包规定了只能是函数).由于它几乎拥有闭包的全部特性.它也能够建立一个独立的做用域,一样外部做用域不能访问块做用域的变量.但块做用域能够访问外部做用域.举个栗子:
function foo() { var a = 2; { //经过{} 显示表示块做用域 let b = a; console.log('块做用域内',b) //2 } console.log('块做用域外',b) //b is not defined } foo() 复制代码
说了相同点,说说不一样点:1,保存变量到块做用域,必须经过let声明.2,块做用域不能和函数同样有名称(函数名) 不少不方便使用闭包或者比较麻烦的时候,是能够考虑经过块做用域进行解决.
总结一下通常何时考虑使用闭包:
这部分也是本身工做使用的总结,若是有补充或者不对的地方,欢迎留言指正.
首先看下面的例子:
function CoolModule() { var something = "cool"; var another = [1, 2, 3]; function doSomething() { console.log( something ); } function doAnother() { console.log( another.join( " ! " ) ); } return { doSomething: doSomething, doAnother: doAnother }; } var foo = CoolModule(); foo.doSomething(); // cool foo.doAnother(); // 1 ! 2 ! 3 复制代码
首先咱们对上面这段代码进成分行分析:
私有数据变量:something, another
内部函数:doSomething, doAnother
直接说结论,上面这个例子就是模块模式.它return返回的这个对象也就是模块也被称为公共API(至少书中是这样称呼的).CoolModule()就是模块构造器或者叫模块函数.
注意:
CoolModule() 只是一个函数,必需要经过调用它来建立一个模块实例。若是不执行外部函数,内部做用域和闭包都没法被建立。
我以为这句话有必要延伸说一下.函数调用一次就会建立一个该函数的做用域(不调用就不会建立),包括建立它里面的变量和函数.模块模式:
模块模式须要具有如下2个条件:(这里结合上面的例子,对书中的定义进行说明方便理解)
模块:
表面上看由模块函数(例子中的CoolModule)所返回的对象就是模块.但模块还必须还包含模块函数的内部函数(即闭包函数).只有包含了才能真正称得上是模块.才强调一次这里的模块与模块化里的模块是有区别的,也不是nodejs里的模块.
模块函数:
模块函数也就是模块构造器,例子中的CoolModule().通常它有两个常见用法.
命名将要做为公共API返回的对象
.我以为命名
应该是用错了,应该是修改
即增删改查更好)大多数模块依赖加载器 / 管理器本质上都是将这种模块定义封装进一个友好的 API。 下面就介绍一个简单的模块管理器实现例子(对书中的例子进行逐行解读):
//首先实例化咱们的模块管理器,取名myModules var MyModules=(function Manager() { //做为咱们的模块池,保存全部定义的模块 var modules={}; /** *使用相似AMD的方式定义新模块,接收3个参数 *name:模块名 *deps:数组形式表示所依赖的其余模块 *impl:模块功能的实现 **/ function define(name,deps,impl) { //遍历依赖模块数组的每一项,从程序池中取出对应的模块,并赋值. //循环完后,deps由保存模块名的数组变成了保存对应模块的数组. for (var i=0;i<deps.length;i++) { deps[i]=modules[deps[i]]; } //将新模块存储进模块池,并经过apply注入它所依赖的模块(即遍历后的deps,实际上就是用deps做为impl的入参) modules[name]=impl.apply(impl,deps); } //从模块池中取出对应模块 function get (name) { return modules[name]; } //暴露定义模块和获取模块的两个api return { define: define, get: get } })() 复制代码
说明: 后面书中说了这么一句为了模块的定义引入了包装函数(能够传入任何依赖)
,这里包装函数指的是Manger(),一样也是咱们上节提到的模块函数.首先说明下什么是包装函数.例如函数A当中还有一个函数B.当咱们想要调用函数B的时候,则须要先调用函数A.那么函数A就叫作函数B的包装函数.也就是说咱们想调用某个模块时,须要先调用它的包装函数即这里的Manger().接着是后面那句而且将返回值,也就是模块的 API,储存在一个根据名字来管理的模块列表中。
注意这里的返回值是指impl的返回值.
接着看经过管理器来定义和使用模块
MyModules.define('bar',[],function () { function hello (who) { return "Let me introduce: " + who; } //返回公共API 即提供一个hello的接口 return { hello:hello }; }); MyModules.define('foo',['bar'],function (bar) { var hungry = "hippo"; functin awesome () { //这里的bar为返回模块bar返回的公共API console.log( bar.hello( hungry ).toUpperCase() ); } //返回公共API 即提供一个awesome的接口 return { awesome:awesome } }) var bar=MyModules.get('bar');//经过管理器获取模块'bar' var foo=MyModules.get('foo');//经过管理器获取模块'foo' console.log( //调用模块bar的hello接口 bar.hello( "hippo" ) ); // Let me introduce: hippo //调用模块foo的awesome接口 foo.awesome(); // LET ME INTRODUCE: HIPPO 复制代码
这节的主要内容仍是了解如今是如何对模块进行一个规范处理.主要是两部份内容,一个是经过名称和依赖合理定义模块并储存.另外一个则是经过名称对存储的模块的调用.其实还能够再增长一个删除模块的方法.
ok,这节说的模块,就是咱们常说的模块化开发.而且主要提到的就是ES6里经常使用的import.没什么好说的.
吐槽: 同一个函数概念在5.5这一个小节里,竟然换着花样蹦出了三个名字!一会叫模块构造器!一会叫模块函数!以及最后的包装函数!每变化一次,都得想一遍它指的是啥!真的是无力吐槽了!!!!
闭包:当函数能够记住并访问所在的词法做用域,而且函数是在当前词法做用域以外执行,这时 就产生了闭包。
模块有两个主要特征:
由于this 提供了一种更优雅的方式来隐式“传递”一个对象(即上下文对象)引用,所以能够将 API 设计得更加简洁而且易于复用。
下面两种常见的对于 this 的解释都是错误的(看看就好,就不过多解读了,以避免增长了对错误的印象)。
人们很容易把 this 理解成指向函数自身.
具名函数,能够在它内部能够使用函数名来引用自身进行递归,添加属性等。(这个知识点其实在第三章提过,既然这里又提了一遍,我也再说一遍.)例如:
function foo() { foo.count = 4; // foo 指向它自身 } 复制代码
匿名函数若是想要调用自身则,须要使用arguments.callee
不过这个属性在ES5严格模式下已经禁止了,也不建议使用.详情能够查看MDN的说明.
切记: this 在任何状况下都不指向函数的词法做用域。你不能使用 this 来引用一个词法做用域内部的东西。 这部分只需记住这一段话就行.
终极疑问: JavaScript里的做用域究竟是对象吗? 这小节最令我在乎的是里面这句话"在 JavaScript 内部,做用域确实和对象相似,可见的标识符都是它的属性。可是做用域“对象”没法经过 JavaScript代码访问,它存在于JavaScript 引擎内部。"它让我想起了最开始学JS的一个疑问,JavaScript里的做用域究竟是对象吗.虽然"在JS里万物皆对象".可是做用域给人的感受却不像是一个对象.更像是一个范围,由函数的{}
围城的范围,限制了其中变量的访问.但直觉告诉我它和对象仍是应该有点联系的.直到读到书中的这段话,更加印证了个人感受. 在JavaScript里,做用域实际上是一个比较特殊的对象,做用域里全部可见的标识符都是它的属性.只是做用域对象并不能经过JavaScript代码被咱们访问,它只存在于JavaScript引擎内部.因此做用域做为一个"对象"是常常被咱们忽略.
this 是在运行时(runtime)进行绑定的,并非在编写时绑定,它的上下文(对象)取决于函数调用时的各类条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。
当一个函数被调用时,会建立一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程当中用到。(PS:因此this并不等价于执行上下文)
经过上节咱们知道,this的绑定与函数的调用位置有关.那调用位置是什么.调用位置就是函数在代码中被调用的位置(而不是声明的位置)。
要寻找调用位置,最重要的是要分析调用栈(就是为了到达当前执行位置所调用的全部函数)。咱们关心的调用位置就在当前正在执行的函数的前一个调用中。PS:调用栈实际上是一个解释起来有点复杂的概念.这里我就不过多解释,这里推荐一篇文章,解释得不错.
这节书里的例子解释得不错,这里就不复制代码了.其实分析调用栈只是为了在运行时找到咱们关心的函数到底在哪里和被谁调用了. 可是实际别写代码时,其实并不会分析得这么清楚的,咱们仍是只需记住this的指向就是咱们调用该函数的上下文对象.意思就是咱们在哪里调用该函数,this就指向哪里
.而且查看调用栈还能够经过浏览器的开发者工具,只需在疑惑的代码上一行加上debugger便可.浏览器在调试模式时,咱们就能够在调用列表里查看调用栈.咱们通常也仅在查找bug时,会使用该方法.
在找到调用位置后,则须要断定代码属于下面四种绑定规则中的哪种.而后才能对this进行绑定.
注意: this绑定的是上下文对象,并非函数自身也不是函数的词法做用域
什么是独立函数调用:对函数直接使用而不带任何修饰的函数引用进行调用.简单点一个函数直接是func()
这样调用,前面什么都没有.不一样于经过对象属性调用例如obj.func()
,也没有经过new关键字new Function()
;也没有经过apply,bind,call强制改变this指向.
默认绑定: 当被用做独立函数调用时(不论这个函数在哪被调用,无论全局仍是其余函数内),this默认指向到window;
注意: 若是使用严格模式(strict mode),那么全局对象将没法使用默认绑定,所以 this 会绑定到 undefined.
隐式绑定: 函数被某个对象拥有或者包含.也就是函数被做为对象的属性所引用.例如obj.func()
.此时this会绑定到该对象上.
隐式丢失: 无论是经过函数别名或是将函数做为入参形成的隐式丢失.只需找到它真正的调用位置,而且函数前没有任何修饰也没有显式绑定(下节会讲到)(非严格模式下).那么this则会进行默认绑定,指向window.
注意: 实际工做中,大部分this使用错误都是由对隐式丢失的不理解形成的.记住函数调用前没有任何修饰和显式绑定(其实就是call、apply、bind),this就指向window
在分析隐式绑定时,咱们必须在一个对象内部包含一个指向函数的属性,并经过这个属性间接引用函数,从而把 this 间接(隐式)绑定到这个对象上。若是咱们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,此时则须要显式绑定.
显式绑定: 能够直接指定 this 的绑定对象,被称之为显式绑定。基本上就是咱们常使用的call、apply、bind方法都是显式绑定.(若是这三个方法不能熟练使用的,建议找度娘或者谷娘学习后,再看这节.)
注意: 若是你传入了一个原始值(字符串类型、布尔类型或者数字类型)来看成 this 的绑定对 象,这个原始值会被转换成它的对象形式(也就是new String(..)、new Boolean(..)或者 new Number(..))。这一般被称为“装箱”。
硬绑定: 使用call、apply、bind方法强制显式地将this进行绑定,称之为硬绑定。 硬绑定的典型应用场景就是建立一个包裹函数(其实就是常说的封装函数),传入全部的参数并返回接收到的全部值. 在封装函数中,咱们常使用apply.一方面是由于它能够手动绑定this,更重要的是由于能够用apply的第二个参数,方便地注入全部传入的参数.例如以前提到的modules[name]=impl.apply(impl,deps)
.由于咱们不知道传入的参数有多少个,但咱们能够方便地使用一个deps
将其所有注入.另外一个经常使用的是foo.apply( null,argue)
当咱们将apply的第一个参数设置为null时,此时this就会默认绑定到window.切记使用这种用法时确保函数foo内没有使用this. 不然极可能会形成全局污染.若是是第三方库的函数就建议不要使用了,由于你不知作别人的函数是否使用了this(关于这部份内容,下节会继续提到).还有一种经常使用就是foo.call( this)
.这样foo里的this都会指向当前调用的上下文环境.
API调用的“上下文”: 第三方库的许多函数,以及 JavaScript 语言和宿主环境中许多新的内置函数,都提供了一个可选的参数,一般被称为“上下文”(context),其做用和 bind(..) 同样,确保你的回调函数使用指定的 this。
JavaScript 中 new 的机制实际上和面向类的语言彻底不一样。在 JavaScript 中,构造函数只是一些 使用 new 操做符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上, 它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操做符调用的普通函数而已。实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操做。
示例:
function foo(a) { this.a = a; } var bar = new foo(2); console.log( bar.a ); // 2 复制代码
使用 new 来调用 foo(..) 时,咱们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。
说明:对于上面这句话进行解释下,若是在一个函数前面带上 new 关键字来调用, 那么背地里将会建立一个链接到该函数的 prototype 的新对象,this就指向这个新对象;
直接上结论:
new绑定=显示绑定>隐式绑定>默认绑定
说明: new绑定与显示绑定是不能直接进行测试比较,但经过分析发现new绑定内部实际上是使用了硬绑定(显示绑定的一种),因此new绑定和显示绑定优先级应该差很少.但话说回来,通常实际使用时,不会这种复杂的交错绑定.因此只需记住下面的断定便可.
判断this:
如今咱们能够根据优先级来判断函数在某个调用位置应用的是哪条规则。能够按照下面的顺序来进行判断:
若是你把 null 或者 undefined 做为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则,this会绑定到window上.
使用情景:
一种很是常见的作法是使用 apply(..) 来“展开”一个数组(也能够用来方便地参数注入),并看成参数传入一个函数。相似地,bind(..) 能够对参数进行柯里化(预先设置一些参数).经过自带bind方法实现柯里化是很方便的,比本身写要简化好多.
注意:
更安全的this
若是函数内使用了this,直接使用null则可能会对全局形成破坏.所以咱们能够经过建立一个“DMZ”(demilitarized zone,非军事区)对象——它就是一个空的非委托的对象(委托在第 5 章和第 6 章介绍)。让this绑定到这个"DMZ上.这样就不会对全局形成破坏. 怎么建立DMZ呢.就是经过Object.create(null) 建立一个空对象.这种方法和 {} 很像,可是并不会建立 Object.prototype 这个委托,因此它比 {}“更空”更加安全.
PS:实际使用通常不会遇到这种状况(也多是我太菜,没遇到),若是函数内有this,那确定是有须要调用的变量或函数,直接把它绑定到一个空对象上.那什么都取不到,还有什么意义?因此函数没有this就传入null.若是有this就把它绑定到真正须要它的对象上,而不是一个空对象上.这些是我本身的看法,若是有不妥的,欢迎留言指正.
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 其实就是foo() 此时this默认绑定到window 复制代码
例子中的间接引用实际上是对函数的理解不深形成的.其实(p.foo = o.foo)()就是(foo)(),这样就是全局调用foo()因此this默认就绑定到了window上.
注意: 对于默认绑定来讲,决定 this 绑定对象的并非调用位置是否处于严格模式,而是 函数体是否处于严格模式。若是函数体处于严格模式,this 会被绑定到 undefined,不然 this 会被绑定到全局对象。(对于这段话其实在2.2.1节就应该说了!)
硬绑定会大大下降函数的灵活性,使用硬绑定以后就没法使用隐式绑定或者显式绑定来修改 this。这时候则须要使用软绑定.
Tips: 这里给的软绑定方法仍是挺好的.可是建议仍是在本身的代码里使用,并注释清除.以避免别人使用,对this错误的判断.
ES6 中介绍了一种没法使用上面四条规则的特殊函数类型:箭头函数。
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)做用域来决定 this。(而传统的this与函数做用域没有任何关系,它只与调用位置的上下文对象有关.这点在本章开头就已经反复强调了.)
重要:
注意: 这种状况:
function module() { return this.x; } var foo = { x: 99, bar:module.bind(this) //此时bind绑定的this为window. } var x="window" console.log(foo.bar())//window 复制代码
在 ES6 以前咱们就已经在使用一种几乎和箭头函数彻底同样的模式:
function foo() { var self = this; // lexical capture of this setTimeout( function(){ console.log( self.a ); }, 100 ); } var obj = { a: 2 }; foo.call( obj ); // 2 复制代码
虽然 self = this 和箭头函数看起来均可以取代 bind(..),可是从本质上来讲,它们想替代的是 this 机制。(的确是这样,我通常会用me替代self.由于少两个单词=.=)
关于this的编码规范建议:
在本身实际工做中,实际上是两种混用的,绝大部分状况下都会使用词法做用域风格.由于有时候你真的很难作到彻底统一.我如今的习惯是,在写任何函数时,开头第一个就是var me =this;
这样在看到函数第一眼,就知道:哦,这个函数是用词法做用域风格的.尤为函数内涉及到回调.这样就避免了写着写着发现this绑定到其余地方去了,一个函数里面this不统一的状况.
(这里总结得很好,我就所有copy了) 若是要判断一个运行中函数的 this 绑定,就须要找到这个函数的直接调用位置。找到以后就能够顺序应用下面这四条规则来判断 this 的绑定对象。
必定要注意,有些调用可能在无心中使用默认绑定规则。若是想“更安全”地忽略 this 绑定,你能够使用一个 DMZ 对象,好比 ø = Object.create(null),以保护全局对象。
ES6 中的箭头函数并不会使用四条标准的绑定规则,而是根据当前的词法做用域来决定 this,具体来讲,箭头函数会继承外层函数调用的 this 绑定(不管 this 绑定到什么)。这其实和 ES6 以前代码中的 self = this 机制同样。
特别注意: 其中最须要注意的就是当你使用jquery或vue时,此时this是被动态绑定了的.大多数 jQuery 方法将 this 设置为已选择的 dom 元素。使用 Vue.js时,则方法和计算函数一般将 this 设置为 Vue 组件实例。vue文档中全部的生命周期钩子自动绑定 this 上下文到实例中,所以你能够访问数据,对属性和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos())。这是由于箭头函数绑定了父上下文,所以 this 与你期待的 Vue 实例不一样,this.fetchTodos 的行为未定义。
也包括使用第三方ajax时,例如axios.解决方法也很简单,要么使用传统的function或者使用let _this=this
进行接管.其实当你使用vue时,你默认的思想就是this指的就是vue实例.因此除了钩子函数和axios里会有点影响外,其他还好.
PS 这里再补充说明 上下文(对象)与函数做用域的区别于联系:
对象能够经过两种形式定义:声明(文字)形式(就是常说的对象字面量)和构造形式。
var myObj = { key: value // ... }; 复制代码
var myObj = new Object(); myObj.key = value; 复制代码
构造形式和文字形式生成的对象是同样的。惟一的区别是,在文字声明中你能够添加多个 键 / 值对,可是在构造形式中你必须逐个添加属性。 PS:其实咱们绝大部分状况下都是使用对象字面量形式建立对象.
在JavaScript中一共有6中主要类型(术语是"语言类型")
简单数据类型: 其中string、boolean、number、null 和 undefined属于简单基本类型,并不属于对象. null 有时会被看成一种对象类型,可是这其实只是语言自己的一个 bug,即对 null 执行typeof null 时会返回字符串 "object"。实际上,null 自己是基本类型。
PS: 缘由是这样的,不一样的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判 断为 object 类型,null 的二进制表示是全 0,天然前三位也是 0,因此执行 typeof 时会返回“object”。
对象:
对象除了咱们本身手动建立的,JavaScript其实内置了不少对象,也能够说是对象的一个子类型.
内置对象:
在 JavaScript 中,这些内置对象实际上只是一些内置函数。这些内置函数能够看成构造函数(由 new 产生的函数调用——参见第 2 章)来使用.
几点说明:
对象属性:由一些存储在特定命名位置的(任意类型的)值. 属性名:存储在对象容器内部的属性的名称.属性值并不会存在对象内.而是经过属性名(就像指针,从技术角度来讲就是引用)来指向这些值真正的存储位置(就像房门号同样).
属性名的两种形式:
.
操做符.也是咱们最经常使用的形式.它一般被称为"属性访问". .
操做符会要求属性名知足标识符的命名规范.[".."]
语法进行访问.这个一般被称为"键访问".[".."]
语法能够接受任意UTF-8/Unicode 字符串做为属性名。而且[".."]
语法使用字符串来访问属性,若是你的属性名是一个变量,则能够使用书中的例子myObject[idx]
形式进行访问.这也是最常使用"键访问"的状况.但若是idx是属性名则仍是需写成myObject["idx"]
字符串形式.注意: 书中说在对象中,属性名永远都是字符串。若是你使用 string(字面量)之外的其余值做为属性名,那它首先会被转换为一个字符串。即便是数字也不例外,虽然在数组下标中使用的的确是数字,可是在对象属性名中数字会被转换成字符串 .
在ES6以前这段话是正确的,可是如今有了symbol. symbol也能够做为对象属性名使用,而且symbol是不能够转化为字符串形式的!
补充: 这里我在书中的例子基础上进行了修改,获得这个例子:
var myObject = { a:2, idx:111 }; var idx="a"; console.log( myObject[idx] ); //2 console.log( myObject["idx"] ); //111 console.log( myObject[this.idx] ); // 2 此时this是指向window.[]里的this一样符合上一章所讲的规则 //结果是否和你所想得同样呢? 复制代码
ES6 增长了可计算属性名,能够在文字形式中使用 [] 包裹一个表达式来看成属性名:
var prefix = "foo"; var myObject = { [prefix + "bar"]:"hello", [prefix + "baz"]: "world" }; myObject["foobar"]; // hello myObject["foobaz"]; // world 复制代码
[]
形式访问储存的值,其中[]
内的值默认形式为数值下标(为从0开始的整数,也就是常说的索引).例如myArray[0]
myArray.baz = "baz"
.注意:添加新属性后,虽然能够访问,但数组的 length 值不会改变.myArray[1]=11;myArray["2"]=22;
这种形式对数组内容进行修改,添加.键/值 对
形式来使用.但JS已经对数组的行为和用途进行了优化.因此仍是建议使用默认的下标/值 对
形式来使用.var newObj = JSON.parse( JSON.stringify( someObj ) );
.但须要指出的是这种方法对于包含function函数或者Date类型的对象则无论用!从 ES5 开始,全部的属性都具有了属性描述符。
var myObject = {}; Object.defineProperty( myObject, "a", { value: 2, writable: true, configurable: true, enumerable: true } ); myObject.a; // 2 //该方法能够配置四个属性描述符 复制代码
注意: 书中关于属性描述符也被称为“数据描述符”
实际上是不够准确的. 对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具备值的属性,该值多是可写的,也可能不是可写的。存取描述符是由getter和setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是二者。(getter和setter是后面立刻要讲到的两个描述符)它们的关系以下:(详情能够查看MDN的解释)
configurable | enumerable | value | writable | get | set | |
---|---|---|---|---|---|---|
数据描述符 | Yes | Yes | Yes | Yes | No | No |
存取描述符 | Yes | Yes | No | No | Yes | Yes |
若是一个描述符不具备value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。若是一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常。
value就是该属性对应的值。默认为 undefined。下面分别介绍剩下的三个属性描述符键值:
除了上面提到的Object.defineProperty(..),ES5还能够经过不少种方法来实现属性或者对象的不可变.
注意: 这些全部方法都是只能浅不变,若是目标对象引用了其余对象(数组、对象、函数,等),其余对象的内容不受影响,仍然是可变的.相似于浅拷贝.
说明: 在 JavaScript 程序中不多须要深不可变性。 有些特殊状况可能须要这样作,可是根据通用的设计模式,若是你发现须要密封或者冻结全部的对象,那你或许应当退一步,从新思考一下程序的设计,让它能更好地应对对象值的改变。
方法:
注意: 你能够“深度冻结”一个对象(连引用的对象也冻结),具体方法为,首先在这个对象上调用 Object.freeze(..), 而后遍历它引用的全部对象并在这些对象上调用 Object.freeze(..)。可是必定要谨慎!由于你引用的对象可能会在其余地发也被引用.
说明: 在 JavaScript 程序中不多须要深不可变性。有些特殊状况可能须要这样作, 可是根据通用的设计模式,若是你发现须要密封或者冻结全部的对象,那你或许应当退一步,从新思考一下程序的设计,让它能更好地应对对象值的改变。
var myObject = { a: 2 }; myObject.a; // 2 复制代码
myObject.a是怎么取到值2的?
myObject.a 经过对象默认内置的[[Get]] 操做(有点像函数调用:[Get]).首先它会在对象中查找是否有名称相同的属性, 若是找到就会返回这个属性的值。若是没有找到名称相同的属性,按照 [[Get]] 算法的定义会执行另一种很是重要的行为。其实就是遍历可能存在的 [[Prototype]] 链,也就是在原型链上寻找该属性。若是仍然都没有找到名称相同的属性,那 [[Get]] 操做会返回值 undefined.
注意: 若是你引用了一个当前词法做用域中不存在的变量,并不会像对象属性同样返回 undefined,而是会抛出一个 ReferenceError 异常.
既然有能够获取属性值的 [[Get]] 操做,就必定有对应的 [[Put]] 来设置或者建立属性.
[[Put]] 被触发时的操做分为两个状况:1. 对象中已经存在这个属性 2. 对象中不存在这个属性.
若是对象中已经存在这个属性,[[Put]] 算法大体会检查下面这些内容:
若是对象中不存在这个属性,[[Put]] 操做会更加复杂。会在第 5 章讨论 [[Prototype]] 时详细进行介绍。
对象默认的 [[Put]] 和 [[Get]] 操做分别能够控制属性值的设置和获取。 目前咱们还没法操做[[Get]] 和 [[Put]]来改写整个对象 ,可是在ES5中能够使用 getter 和 setter 改写部分默认操做,只能应用在单个属性上,没法应用在整个对象上。
注意: 书中后面说的访问描述符
就是存取描述符
.关于属性描述符,存取描述符及数据描述符能够查看MDN的解释)
getter: getter 是一个隐藏函数,会在获取属性值时调用。同时会覆盖该单个属性默认的 [[Get]]操做.当你设置getter时,不能同时再设置value或writable,不然就会产生一个异常.而且当你设置getter或setter时,JavaScript 会忽略它们的 value 和 writable 特性.
语法: {get prop() { ... } }
或{get [expression]() { ... } }
.其中prop
:要设置的属性名. expression
:从 ECMAScript 2015 开始能够使用计算属性名. 使用方式:
var myObject = { a: 1111, //在后面会发现myObject.a为2,这是由于设置了getter因此忽略了value特性. //方式一:在新对象初始化时定义一个getter get a() { return 2 } }; Object.defineProperty( myObject, // 目标对象 "b", // 属性名 { // 方式二:使用defineProperty在现有对象上定义 getter get: function(){ return this.a * 2 }, // 确保 b 会出如今对象的属性列表中 enumerable: true } ); myObject.a = 3; //由于设置了getter因此忽略了writable特性.因此这里赋值没成功 myObject.a; // 2 myObject.b; // 4 delete myObject.a;//能够使用delete操做符删除 复制代码
setter: setter 是一个隐藏函数,会在获取属性值时调用。同时会覆盖该单个属性默认的 [[Put]]操做(也就是赋值操做).当你设置setter时,不能同时再设置value或writable,不然就会产生一个异常.而且当你设置getter或setter时,JavaScript 会忽略它们的 value 和 writable 特性.
语法: {set prop(val) { . . . }}
或{set [expression](val) { . . . }}
.其中prop
:要设置的属性名. val
:用于保存尝试分配给prop的值的变量的一个别名。expression
:从 ECMAScript 2015 开始能够使用计算属性名. 使用方式:
var myObject = { //注意:一般来讲 getter 和 setter 是成对出现的(只定义一个的话 一般会产生意料以外的行为): //方式一:在新对象初始化时定义一个setter set a(val) { this._a_ = val * 2 }, get a() { return this._a_ } }; Object.defineProperty( myObject, // 目标对象 "b", // 属性名 { set: function(val){ this._b_ = val * 3 }, // 方式二:使用defineProperty在现有对象上定义 setter get: function(){ return this._b_ }, // 确保 b 会出如今对象的属性列表中 enumerable: true } ); myObject.a = 2; myObject.b = 3; console.log(myObject.a); //4 console.log(myObject.b);//9 console.log(myObject._a_);//4 console.log(myObject._b_);//9 delete myObject.a;//能够使用delete操做符删除 复制代码
属性存在性: 如何判断一个对象是否存在某个属性(准确来讲是检查这个属性名是否存在),这时就须要用到:
in
操做符 in 操做符会检查属性是否在对象及其 [[Prototype]] 原型链中(参见第 5 章)。注意:
属性可枚举性: 若是一个属性存在,且它的enumerable 属性描述符为true时.则它是可枚举的.而且能够被for..in 循环. 一个属性不只仅须要存在,还须要它的enumerable 为true才是可枚举的,才能被for...in遍历到.
注意: for...in不适合对数组进行遍历,对数组的遍历仍是使用传统的for循环.
对属性的可枚举性判断,则须要用到如下几种方法:
关于这节我以为仍是以理清for..in和for..of为主.后面延伸的@@iterator及Symbol.iterator的使用,不必过于深究.注意书中123页第二行done 是一个布尔值,表示是否还有能够遍历的值。
有个错误,应该改为done 是一个布尔值,表示遍历是否结束。
不然你在看后面它的说明时会感受到自相矛盾.这里我也是以for..in和for..of为主进行说明,也更贴近咱们实际使用.
for..in
for..of
例子比较
let arr = ['shotCat',111,{a:'1',b:'2'}] arr.say="IG niu pi!" //使用for..in循环 for(let index in arr){ console.log(arr[index]);//shotCat 111 {a:'1',b:'2'} IG niu pi! } //使用for..of循环 for(var value of arr){ console.log(value);//shotCat 111 {a:'1',b:'2'} } //注意 for..of并无遍历获得` IG niu pi!`.缘由我前面说过`它只循环数组里存放的值,不会涉及到对象里的key.`更不用说 [[Prototype]] 链.(for..in则会) 复制代码
如何让对象也能使用for..of ?
你能够选择使用书中的本身经过Object.defineProperty()定义一个Symbol.iterator属性来实现.这里我就不赘述了.也是最接近原生使用感觉的.不过我这里要介绍一个稍微简单点的方法来实现.就是使用上节讲到的Object.keys()搭配使用.举个例子:
var shotCat={ name:'shotCat', age:'forever18', info:{ sex:'true man', city:'wuhan', girlFriend:'新垣结衣!' } } for(var key of Object.keys(shotCat)){ //使用Object.keys()方法获取对象key的数组 console.log(key+": "+shotCat[key]); } 复制代码
书中小结总结得挺全的,这里我就搬运下
注意: 正如书中提示的那样,整章一半以上几乎都是讲面向对象和类的概念.会读得人云里雾里,给人哦,也许大概就是这样子
的感受.后面我仍是会对那些抽象的概念找到在JavaScript里对应的"立足点",不至于对这些概念太"飘".
说明:
注意: Javascript语言不支持“类”,所谓的"类"也是模拟出的“类”。即便是ES6引入的"类"实质上也是 JavaScript 现有的基于原型的继承的语法糖。
一句话:类其实也是一种设计模式!
JavaScript 只有一些近似类的语法元素 (好比 new 和 instanceof),不过在后来的 ES6 中新增了一些元素,好比 class 关键字,其实质上也是 JavaScript 现有的基于原型的继承的语法糖。也不是真正的类.
这部分书中的描述,我理解起来也比较费劲,主要是它提到的栈,堆与我理解中内存里的栈,堆相冲突了.这里简单说下个人理解,若有误,感激指正.
stack类实际上是一种数据结构.它能够储存数据,并提供一些公用的方法(这和上面提到的类很类似).可是stack类其实只是一个抽象的表示,你想对它进行操做,就须要先对它进行实例化.
这节主要就是说明"类"和"实例"的关系. 在JavaScript里"类"主要是构造函数,"实例"就是对象.
一个类就像一张蓝图。为了得到真正能够交互的对象,咱们必须按照类来实例化一个东西,这个东西(对象)一般被称为实例,有须要的话,咱们能够直接在实例上调用方法并访问其全部公有数据属性。
总而言之:类经过实例化获得实例对象.
在面向类的语言中,你能够先定义一个类,而后定义一个继承前者的类。后者一般被称为“子类”,前者一般被称为“父类”。子类能够继承父类的行为,而且能够根据本身的需求,修改继承的行为(通常并不会修改父类的行为).注意:咱们讨论的父类和子类并非实例,在JavaScript里类通常都是构造函数。
大概你看了它的"解释",对多态仍是懵懵懂懂.这里我再解释下:
什么是多态?
同一个操做,做用于不一样的对象,会产生不一样的结果。发出一个相同的指令后,不一样的对象会对这个指令有不一样的反应,故称为多态。 说明: 书中例子中的inherited其实就是至关于super.而且注意书中的这些例子都是伪代码! 并非真的在JavaScript里就是这样实现的.补充:这里是关于super的mdn连接.
当调用方法时会自动选择合适的定义
,这句话怎么理解,当子类实例化后,执行drive()方法时,它并不会直接去执行父类的drive().而是子类上的drive().简单来讲就是实例来源于那个类,它就使用那个类的方法.说明:
多重继承: 一个子类能够继承来自多个父类的方法.
多重继承引起的问题: 多重继承可能会出现,多个父类中方法名冲突的问题,这样子类
到底引用哪一个方法?
多重继承与JavaScript: JavaScript自己并无提供多重继承功能.但它能够经过其余方法来达到多重继承的效果.
JavaScript 中只有对象,并不存在能够被实例化的“类”。一个对象并不会被复制到其余对象,它们会被关联起来(参见第 5 章)(其实就是引用,因此它的多态是"相对"的)。 因为在其余语言中类表现出来的都是复制行为,所以 JavaScript 开发者也想出了一个方法来模拟类的复制行为,这个方法就是混入(就是经过混入来模拟实现类的多重继承)。
郑重提醒: 书中这里的类都是对象形式的.例子里的sourceObj, targetObj,这就可能形成一个"误导",在JavaScript里是没有真正的类,所谓的类也不过是咱们模拟出来的"类",不过是一种语法糖(包括ES6里的class).在JavaScript里"所谓的类"常常是一个构造函数,你并不能这样进行遍历,只能对它的实例对象进行这种操做.不要被书中例子带进去了,不要混淆,毕竟咱们最终使用的是JavaScript(而不是其余面向对象的语言.),它里面的类经常并非一个对象!
显式混入: 书中没有给出明确的显式混入的定义,可是读完整章.基本就知道什么是显式混入了.显式混入就是经过相似mixin()方法,显式地将父对象属性逐一复制,或者有选择地复制(即例子中的存在性检查)到子对象上.
显式混入经常使用方法: 就是书中的例子, 首先有子对象,并对其进行特殊化(定义本身的属性或方法).而后再经过mixin()方法将父对象有选择地复制(即存在性检查,过滤子对象已有的属性,避免冲突)到子对象上.
显式混入注意点: 显式混入时,切记一点你要避免父对象的属性与子对象特殊化的属性冲突.这就是为何例子中要进行存在性检查,以及后面要说的混合复制,可能存在的重写风险.
1. 再说多态(其实说的就是js里的多态)
显式多态: 将父对象里的方法经过显式绑定到子对象上.就是显式多态.例如书中的例子:Vehicle.drive.call( this )。显式多态也是为了JS来模拟实现多重继承的!
说明: 在ES6以前是没有相对多态的机制。因此就使用call这种进行显式绑定实现显式动态.注意JavaScript里实现多态的方法也被称为"伪多态".因此不要对后面忽然冒出的伪多态概念而一脸懵逼(其实整本书常常作这样的事)
显式多态(伪多态)的缺陷: 由于在JavaScript 中存在屏蔽(实际是函数引用的上下文不一样),因此在引用的时候就须要使用显式伪多态的方法建立一个函数关联. 这些都会增长代码的复杂度和维护难度(过多的this绑定,真的会让代码很难读)。
2. 混合复制(显式混入另外一种不经常使用方法)
前面的显式混入的方法是先有子对象并进行特殊化,而后再有选择地复制父对象属性.这个不经常使用的方法则是反过来的,结合书中例子,它先用一个空对象彻底复制父对象的属性,而后该对象复制特殊化对象的属性,最后获得子对象.这种方法明显是比第一种麻烦的,而且在复制特殊化对象时,可能会对以前重名的属性(即复制获得的父对象属性)进行重写覆盖.因此这种方法是存在风险,且效率低下的.
显式混入的缺陷:
3. 寄生继承
显式混入模式的一种变体被称为“寄生继承”,它既是显式的又是隐式的. 首先会复制一份父类(对象)的定义,而后混入子类(对象)的定义(若是须要的话保留到父类的特殊引用),而后用这个复合对象构建实例。
说明: 寄生继承与混合复制是很类似的,最大的区别是寄生继承是经过实例化构造函数(JS中的"类")来实现复制的.
隐式混入: 它与显示混入最大的区别,就是它没有明显的对父类(对象)属性进行复制的过程.它是经过在构造函数调用或者方法调用中使用显式绑定例如: Something.cool.call( this)来实现混入(多重继承).其本质就是经过改变this指向来实现混入.
整章的重点其实就是让你理解什么叫类.除了最后一小节的混入和JavaScript有那么一点点关系.其他的小结和JavaScript都没什么关系.重要的是理解类这种思想和设计模式.
重点:
注意:本章的前提是你已经比较熟悉原型及原型链.不太熟或者不知道的能够,经过这篇文章熟悉下.
[[Prototype]]
JavaScript 中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对于其余对象的引用(通常就是其构造函数prototype属性的引用)。几乎全部的对象在建立时 [[Prototype]] 属性都会被赋予一个非空的值。
吐槽: 书中有这样一句话 "注意:很快咱们就能够看到,对象的 [[Prototype]] 连接能够为空,虽然不多见。"我前先后后看了三遍都没找到它所说的对象的 [[Prototype]] 连接能够为空.
的状况!应该是做者写忘记了.ok,这里我来讲下对象的 [[Prototype]] 连接能够为空
的状况.就是经过Object.create(null)获得的对象.它的 [[Prototype]] 是为空的.应该说它的全部都是空的.为何?由于null是原型链的顶端.它是没有[[Prototype]]的.对应的能够对比下console.log(Object.create({}))
和console.log(Object.create(null))
[[Prototype]]有什么用?
我原觉得做者会说能够做为存放实例对象的公共属性,而后像类同样讲得更深入点.不过此次只是说了它代表的做用.
做用: 就是存放哪些不在对象自身的属性. 当咱们访问一个对象的属性时,此时对象的内部默认操做[[Get]],首先会检查对象自己是否有这个属性,若是有的话就使用它。若是没有的话,[[Get]] 就会继续访问对象的 [[Prototype]] 链.([[Prototype]]其实就是其构造函数的prototype属性.也是一个对象.)若是找到,就返回该属性值.若是没有就继续寻找下一个[[Prototype]]链.直到找完整条[[Prototype]]链.仍是没有的话,[[Get]] 就会返回undefined.
补充:
全部普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype。(Object.prototype的[[Prototype]] 最终会指向null.null就是最后的终点). 这个 Object.prototype 对象,包含了 JavaScript 中许多通用的功能,例如:toString() , valueOf(), hasOwnProperty(..)和 isPrototypeOf(..)。
说明: 看完本节时,切记不要对myObject.foo = "bar"
这种简单的对象属性赋值产生顾虑和疑惑.这种赋值绝对不会对原型链产生任何影响!基本也不会出现赋值不成功的状况.若是有人敢在团队项目里修改对象的属性描述符,早就被拖出去打死了!!! 这部分能够看作补充知识,知道有这些奇葩设定就行.其实这节更应该关注的是myObject.foo
的返回值.
注意: 书中提到的动词屏蔽
其实指的就是在对象上建立同名属性(原型链上已有该属性).注意不要被绕晕了.还有++就至关于myObject.a=myObject.a+1,注意分解就行,不存在什么特别须要小心的.
吐槽:模仿类居然被说成奇怪的无耻滥用!?不这样作,js那些高级用法怎么实现?怎么会有如今前端的百花齐放(轮子满地跑)?这也是冒得办法的办法啊!毕竟当时js只是小众,不期望它有多大能耐.毕竟只是一我的用7天"借鉴"出来的东西.
"类"函数: JavaScript用来模仿类的函数就被称为类函数,其实就是咱们常说的构造函数.
"类"函数模拟类的关键: 全部的函数默认都会拥有一个名为 prototype 的公有而且不可枚举(参见第 3 章)的属性,它会指向另外一个对象.当咱们经过new 函数(构造函数)来获得实例对象时,此时new会给实例对象一个内部的 [[Prototype]]属性,实例对象内部的[[Prototype]]属性与构造函数的prototype属性都指向同一个对象.那JS的这个特性怎么模拟类呢?首先类的本质就是复制!.明白这点后,咱们就须要实现伪复制.咱们能够将类里的属性,放在函数的prototype属性里.这样该函数的实例对象就能够经过[Prototype]访问这些属性.咱们也常常把这种行为称为原型继承(做者后面会疯狂吐槽这个称呼,我后面再解释为何吐槽).这样就实现了伪"复制". 能够达到和类类似的效果.
注意: 虽说全部的函数默认都会拥有一个名为 prototype属性.但也有特殊的时候.就不是默认的状况.就是经过bind()硬绑定时.所返回的绑定函数,它是没有prototype属性的!
图解真正的类与JS的模拟类:
关于原型继承这个名字的疯狂吐槽: 做者的吐槽主要集中在"继承"两个字,缘由是在面向类的语言中,"继承"意味着复制,但在JavaScript里原型继承却根本不是这个意思,它并无复制,而是用原型链来实现.因此疯狂吐槽其误导.
什么是差别继承? 我根本没听过这个术语,初次看做者所谓的解释,这是啥?他想说啥?后来读了好多遍,终于大概理解了.若是你也看不懂做者想在表达什么,就pass这部分.不必理解.反而会把你看得更迷惑. 好了,我来解释下什么叫差别继承.差别继承就是原型继承的一个不经常使用的别名.咱们知道对象能够经过原型链继承一部分属性,但咱们仍能够给对象设置其余有差别不一样的属性.这也就能够称为差别继承.
构造函数之因此是构造函数,是由于它被new调用,若是没被new调用,它就是一个普通函数.实际上,new会劫持全部普通函数并用构造对象的形式来调用它,而且不管如何都会构造返回一个对象.
关于两种“面向类”的技巧,我这就不说明了,理解了这部分第一第二章关于this的使用,就很简单了.
prototype.constructor: 为了正确理解constructor.我特地在标题上加上prototype.是想强调:一个对象访问constructor时,会默认访问其原型对象上的constructor属性.
注意:
function Foo() { /* .. */ } Foo.prototype = { /* .. */ }; // 有时候咱们会须要建立一个新原型对象,所以也不会有默认的constructor属性指向构造函数 // 须要在 Foo.prototype 上“修复”丢失的 .constructor 属性 // 关于 defineProperty(..),参见第 3 章 Object.defineProperty( Foo.prototype, "constructor" , { enumerable: false,//不可枚举 writable: true, configurable: true, value: Foo // 让 .constructor 指向 Foo } ); //上面这种方法是比较严谨,也比较麻烦的.而且使用Object.defineProperty()风险是很大的. //因此咱们实际是这样修改的 Foo.prototype.constructor=Foo; //直接将其赋值Foo 惟一要注意的是此时constructor是可枚举的.会被实例对象的for..in..遍历到. 复制代码
原型对象到原型对象的继承: 例如:Bar.prototype 到 Foo.prototype 的委托关系, 正确的JavaScript中“原型风格”:
function Foo(name) { this.name = name; } Foo.prototype.myName = function() { return this.name; }; function Bar(name,label) { Foo.call( this, name ); this.label = label; } // 咱们建立了一个新的 Bar.prototype 对象,而且它的[[Prototype]] 关联Foo.prototype Bar.prototype = Object.create( Foo.prototype ); // 注意!Object.create()是返回一个新的对象,因此如今没有 Bar.prototype.constructor 了 // 若是你须要这个属性的话可能须要手动修复一下它 Bar.prototype.myLabel = function() { return this.label; }; var a = new Bar( "a", "obj a" ); a.myName(); // "a" a.myLabel(); // "obj a" 复制代码
错误用法:
Bar.prototype = Foo.prototype;
此时并不会建立一个关联到 Bar.prototype 的新对象,它只是让 Bar.prototype 直接引用 Foo.prototype 对象。 所以当你执行相似 Bar.prototype. myLabel = ... 的赋值语句时会直接修改 Foo.prototype 对象自己。Bar.prototype = new Foo();
它使用 了 Foo(..) 的“构造函数调用”,若是函数 Foo 有一些其余操做的话,尤为是与this有关的的话,就会影响到 Bar() 的“后代”,后果不堪设想。结论: 要建立一个合适的关联对象,咱们需使用 Object.create(..) 而不是使用具备反作用的 Foo(..)。这样作惟一的缺点就是须要建立一个新对象而后把旧对象抛弃掉(主要是须要手动设置constructor),不能直接修改已有的默认对象。
检查"类"关系
a instanceof Foo
。b.isPrototypeOf( a );
注意: 若是使用内置的 .bind(..) 函数来生成一个硬绑定函数(参见第 2 章)的话, 该函数是没有 .prototype 属性的。若是硬绑定函数instanceof 的话,则其bind的 目标函数的prototype会成为硬绑定函数的prototype.
关于__proto__: 咱们知道函数能够直接经过prototype属性直接访问原型对象.那对象怎么访问呢?咱们知道是经过[[prototype]]链.怎么访问呢? 在ES5之中的标准方法:经过Object.getPrototypeOf( )方法来获取对象原型.Object.getPrototypeOf( a ) === Foo.prototype; // true
, 另外一种方法:在 ES6 以前并非标准,但却被绝大多数浏览器支持的一种方法,能够访问内部[[prototype]]对象.那就是__proto__
.例如:a.__proto__ === Foo.prototype; // true
.你甚至能够经过.__proto__.__ptoto__...
来访问整个原型链. .__proto__
实际上并不存在于你正在使用的对象中.而且它看起来很像一个属性,可是实际上它更像一个 getter/setter(见第三章).
[[Prototype]] 机制就是存在于对象中的一个内部连接,它会引用其余对象。
这个连接的做用是:若是在对象上没有找到须要的属性或者方法引用,引擎就会继续在 [[Prototype]] 关联的对象上进行查找。同理,若是在后者中也没有找到须要的 引用就会继续查找它的 [[Prototype]],以此类推。这一系列对象的连接被称为原型链。
问:"咱们已经明白了为何 JavaScript 的 [[Prototype]] 机制和类不同,也明白了它如何创建对象间的关联。"
答: 类的机制是复制,JavaScript里原型链的机制是引用.
问:"那 [[Prototype]] 机制的意义是什么呢?为何 JavaScript 开发者费这么大的力气(模拟类)在代码中建立这些关联呢?"
答: 意义就是模拟类,JavaScript不须要复制(我以为这不是个优势)而经过原型链实现"实例"对"类"的"继承(其实就是引用)".这样就达到了实例对象对某些属性(即原型对象里的属性)的复用.
Object.create(..)
这个方法其实咱们在前面已经使用过不少次."Object.create(..) 会建立一个新对象(bar)并把它关联到咱们指定的对象(foo),这样咱们就能够充分发挥 [[Prototype]] 机制的威力(委托)而且避免没必要要的麻烦(好比使用 new 的构造函数调用会生成 .prototype 和 .constructor 引用)。"实际上这个方法就是建立返回一个新对象,这个新对象的原型([[Prototype]])会绑定为咱们输入的参数对象foo.而且因为不是经过构造函数的形式,因此不须要为函数单独设置prototype.虽然Object.create(..)很好,但实际咱们使用的更多的仍是构造函数形式.
注意: Object.create(..) 的第二个参数指定了须要添加到新对象中的属性名以及这些属性的属性描述符(参见第 3 章)。
Object.create(null)
这个方法其实咱们在前面也讲解过几回."Object.create(null) 会建立一个拥有空(或者说null)[[Prototype]] 连接的对象,这个对象没法进行委托。因为这个对象没有原型链,因此 instanceof 操做符(以前解释过)没法进行判断,所以老是会返回 false。 这些特殊的空 [[Prototype]] 对象一般被称做“字典”,它们彻底不会受到原型链的干扰,所以很是适合用来存储数据。"
"Object.create()的polyfill代码."这部分我就不作解读了,由于如今都8102年,es6早就普及了,你几乎不可能再用到es5以前的语法了.因此这部分你们了解下便可.
[[Prototype]] 的本质做用: 书中提到了一个观点"处理“缺失”属性或者方法时的一种备用选项。"(即备用设计模式).但随后进行了否认"可是这在 JavaScript 中并非很常见。因此若是你使用的是这种模式,那或许应当退后一步并从新思考一下这种模式是否合适。" 做者给出的观点是:"进行委托设计模式,即例子中的内部委托(就是在对象里套了个壳再引用了一遍,为的是将委托进行隐藏).这样能够使咱们的API设计得更加清晰."文中的清晰是指,当咱们须要引用原型对象的属性方法时,咱们在对象内部设置对应专门的属性(例子中的doCool),进行内部委托(其实就是套个壳进行隐藏).这样咱们对象的属性就是"完整"的.
在实际工做中,咱们经常就是把原型对象做为存放对象的公共属性方法的地方.对于通常比较重要的操做才会在对象里进行内部委托(隐藏委托)!
总结得很好很全面,这里我仍是直接摘抄了,不是偷懒哦!
第 5 章的结论:[[Prototype]] 机制就是指对象中的一个内部连接引用另外一个对象。换句话说,JavaScript 中这个机制的本质就是对象之间的关联关系。在第六章又被称为委托. PS:前面在讲原型的时候我就习惯用父对象指代原型对象(相似"父类"),用子对象指代其实例对象(相似"子类").本章也将采用这种称呼,故下面再也不说明.(其实我以为用父对象和子对象称呼更形象)
一句话:[[Prototype]]机制是面向委托的设计,是不一样于面向类的设计. 下面将分别介绍类理论和委托理论.
类理论设计方法: 首先定义一个通用父(基)类,在 父类类中定义全部任务都有(通用)的行为。接着定义子类 ,它们都继承自 父类而且会添加一些特殊的行为来处理对应的任务,而且在继承时子类能够使用方法重写(和多态)父类的行为.
类理论中许多行为能够先“抽象”到父类而后再用子类进行特殊化(重写)。 ps:这部分了解便可,着重理解下面JavaScript用到的委托.
类理论设计方法: 首先你会定义一个"父"对象(至关于上节中的父类),它会包含全部任务均可以使用(委托)的具体行为。接着,对于每一个任务你均可以定义一个对象("子"对象)来存储对应的数据和行为。你能够把特定的任务对象都关联到父对象上,让它们在须要的时候能够进行委托。 (其实咱们通常都是用父对象来定义通用的方法,子对象进行委托.而后子对象自身个性的属性方法就写在子对象自己,并避免与父对象的属性名冲突)
ps: 这节书中这段话可是咱们并不须要把这些行为放在一块儿,**经过类的复制**,咱们能够把它们分别放在各自独立 的对象中,须要时能够容许 XYZ 对象委托给 Task。
有个错误."经过类的复制"应该改成"经过"[[Prototype]]机制".这里应该是做者的手误. 在 JavaScript 中,[[Prototype]] 机制会把对象关联到其余对象。不管你多么努力地说服自 己,JavaScript 中就是没有相似“类”的抽象机制。(其实主要缘由仍是是JavaScript没有完整的复制机制)
委托理论的使用建议:
PS:书中这里写了3条,其实只有2条,第三条不过是对第一条的说明,这里我进行了合并.
委托理论的使用注意:
实际上,在编写本书时,这个行为被认定是 Chrome 的一个 bug, 当你读到此书时,它可能已经被修复了。
我只想说WTF! 好吧,我知道chrome之前可能出现过这个"bug"了=.=)这节主要是比较了"经过构造函数(模拟类)实现原型继承"与"经过对象关联(委托形式,Object.create( ... ))实现原型继承"两种方式的区别.
结论: 经过对象关联,委托形式,更加简洁,更加清晰易懂.
PS:这里我本来本身对例子画出原型示意图.可是发现是真的复杂,而且和书中简洁后的示意图是差很少的,因此这里就不展现了,省得让读者看得更头大.这里建议,读者本身在草稿纸上画出原型示意图.
其实这节讲得仍是"经过构造函数(模拟类)实现原型继承"与"经过对象关联(委托形式,Object.create( ... ))实现原型继承"两种方式的区别.不过此次主要是之前端实际使用场景进行讲解.
这里我就不以书中的例子进行讲解了,而是直接站在更高的角度对这种"类"风格的代码进行讲解.
最大特色: 1是经过构造函数进行模拟类,2是经过显式伪多态(硬绑定函数)关联两个函数.
注意:
虽然书中对显式伪多态称为"丑陋的",还用了一个语气动词"呸!".虽然这样很差,但有时用call真的很方便,因此用得也不少.
最大特色: 经过对象载体来模拟父子,并经过Object,create(...)来对两个对象进行关联.并经过委托的形式进行引用.与上节中提到的类形式还有一个区别:对象foo构建后,须要手动调用setUp方法进行初始化.故对象的构建与初始化是分开的.而构造函数形式则是在new 构造函数时, 同时进行了对象构建与初始化.(关于这点我下面还会再说明的)
关于书中这句使用类构造函数的话,你须要(并非硬性要求,可是强烈建议)在同一个步骤中实现构造和初始化。然而,在许多状况下把这两步分开(就像对象关联代码同样)更灵活。
的理解:使用类构造函数形式,当咱们使用new 构造函数
时,实际上是在一步实现对象的构建和对象数据的初始化(经过构造函数里的call) ;使用这种委托形式,咱们是分别经过Object.create( ... );
构建对象和foo.setUp( ...);
来初始化的.即咱们是分两步实现的.这样分开的话实际上是更加灵活,也更符合编程中的关注分离原则.
这节也是同样经过二者的对比来突显委托设计模式的各类优势.这里我就再也不对书中的例子进行解读.若是你真正理解了类和委托的话,实际上是很简单的.若是以为复杂的话,能够在纸上理一下函数和对象之间的关系,下面我就只总结下这里提到委托设计模式的优势,固然核心是更简洁.
简洁体如今:
这节主要是介绍ES6提供的2个简洁写法与其中的隐患.
语法:
var Foo = { bar() { /*..*/ },//字面形式声明 }; 复制代码
// 使用更好的对象字面形式语法和简洁方法 var AuthController = { errors: [], checkAuth() { // ... }, server(url,data) { // ... } // ... }; // 如今把 AuthController 关联到 LoginController Object.setPrototypeOf( AuthController, LoginController ); 复制代码
弊端:
吐槽: 纵观整本书,做者关于JavaScript中模拟类和继承"的批评,说它们具备很大误导性!更是嗤之以鼻!就差爆粗口了,JavaScript就像一个异教徒,应该绑在十字架上被烧死!可是他这样的观点,都是站在其余语言的角度来看待时,产生的.我想更多的读者多是只接触过JavaScript.那么他实际上是没有这些疑惑的!!!你反而给他们讲这一大堆其余语言的"正确"含义,有时候会时得其反!让读者更加困惑,若是是理解不透彻的,反而会怀疑本身原本写的是对的代码!因此读者应该作一个能够理解做者意图,而且拥有自我看法和观点立场!
什么是内省(自省)? 首先,本节须要弄懂一个问题,什么是内省,或者是自省。书中的解释是自省就是检查实例的类型。类实例的自省主要目的是经过建立方式来判断对象的结构和功能。
我这里再更通俗地解释下:当咱们构建获得一个实例对象时,有时候咱们是不太清除它的属性和方法的.尤为是第三方库.有时候贸然使用会致使不少错误(例如调用的方法不存在,或者报错等).这个时候咱们就须要经过自省.其实就是经过一系列操做,来确认实例是否是咱们想要的那个,实例的方法是否是咱们想要的(存在且可用).
内省的方法:
function Foo() { // ... } Foo.prototype.something = function(){ // ... } var a1 = new Foo(); // 假设咱们不知道上面的过程,只知道获得实例对象a1 //咱们想知道a1是否是我所但愿的函数Foo所构建的 if (a1 instanceof Foo) { a1.something(); } 复制代码
例子中咱们有一个实例对象a1,可是咱们不知道a1是否是咱们所但愿的函数Foo所构造的,此时就能够经过instanceof
进行判断. instanceof
比较适合判断实例对象和构造函数之间的关系.
缺陷: 可是若是咱们想判断函数A是否是函数B的"子类"时,则会稍微麻烦点,咱们须要像这样A.prototype instanceof B
进行判断.而且也不能直接判断两个对象是否关联.
//若是a1的something存在的话,则咱们能够进行调用 if ( a1.something) { a1.something(); } 复制代码
其实这种方法是很是经常使用的,排除了在不知道存在性状况下,贸然调用的风险.
缺陷: 关于书中提到的缺点,四个字归纳就是"以偏概全" .书中关于Promise的例子,就是以偏概全的例子.因此咱们在使用时,在if判断a1.something存在时,才会在后面使用something方法.不要直接使用anotherthing,这种没确认过的方法.
Object.getPrototypeOf(..)
进行判断.例如Object.getPrototypeOf(a)===A
其中a,A都是对象.若是为true,则说明a的原型链上含有对象A.后面关于<你不知道的JavaScript>中和下.还在写做当中,手头上还有一篇webpack彻底指北的文章,目前写了一半2w字,也都是面向新手,真正的全面地由浅入深.最近,空降一个新项目组,开发到测试只有一个月,还要带新人,更新会很慢.不过我会争取年前所有放出.若是你们喜欢的话,能够关注我一下,文章首发仍是在掘金的.
最后求一个内推,目前笔者在一家软件国企(半养老型).年末或者明年初就会考虑离职.但愿进入一家比较好的互联网企业.若是有小伙伴有好的机会能够发我邮箱:bupabuku@foxmail.com.谢谢!
目前暂时优先考虑武汉(房子,盆友,东西都在这边,去外地太不方便了-.-)
为了方便你们下载到本地查看,目前已经将MD文件上传到百度网盘上了. 连接: pan.baidu.com/s/1ylKgPCw3… 提取码: jags (相信进去后,大家还会回来点赞的! =.=)