全方位完全读懂--一篇六万多字的读书笔记

前言

Q&A

  • 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

  • 3.如何读书:先读厚,再读薄!
    首先先把书读厚: 将每一节里的全部知识点弄懂,不留遗漏.记下全部提到的知识点,并将重要的知识点高亮标识(电子书的话).而后在本身本地的MD笔记里,按照必定的逻辑顺序,尽可能用本身的话语进行阐述总结这些知识点.若是有读几遍也不理解的地方,能够查询MDN,结合本身的实际工做经验,或者先圈起来,继续往下读,随着后面理解的深刻,前面不懂的地方天然也就明了了.这篇读书笔记就是带你怎么把书读厚.
    而后把书读薄: 这部分需读者你本身在完全理解的基础上,并站在全局的角度进行概括去总结.先是按章进行思惟导图式的总结.而后章与章之间进行规律总结,并记住特例.例如:做用域与原型链都有一个相似的"就近原则",因为就近原则因此就产生了"屏蔽".这些都是须要本身站在全局融会贯通的角度去总结.虽然网上有别人总结好的,但咱们不该该养成什么都依赖别人,本身直接复制的习惯(若是你想一直作一个'复制粘贴'程序员的话).

第一部分 做用域和闭包

第一章 做用域是什么

1.1 编译原理

传统编译的三个步骤web

  • 1,分词/词法分析(Tokenizing/Lexing) : 这个过程会将由字符组成的字符串分解成(对编程语言来讲)有意义的代码块,这些代码块被称为词法单元(token)。例如,考虑程序var a = 2;。这段程序一般会被分解成 为下面这些词法单元:var、a、=、二、;。空格是否会被看成词法单元,取决于空格在 这门语言中是否具备意义。
  • 2,解析/语法分析(Parsing): 这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的表明了程序语法结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。var a = 2; 的抽象语法树中可能会有一个叫做 VariableDeclaration 的顶级节点,接下来是一个叫做Identifier(它的值是a)的子节点,以及一个叫做 AssignmentExpression 的子节点。AssignmentExpression 节点有一个叫做 NumericLiteral(它的值是 2)的子节点。
  • 3,代码生成: 将 AST 转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息息相关。抛开具体细节,简单来讲就是有某种方法能够将 var a = 2; 的 AST 转化为一组机器指令,用来建立一个叫做 a 的变量(包括分配内存等),并将一个值储存在 a 中。

说明: 此处只需记住第一步:分词/词法分析.第二步:解析/语法分析,获得抽象语法树(AST).第三步:代码生成,将抽象语法树转换为机器指令.面试

JavaScript与传统编译的不一样点:

  • 1,JavaScript 引擎不会有大量的(像其余语言编译器那么多的)时间用来进行优化.
  • 2,JavaScript与传统的编译语言不一样,它不是在构建以前提早编译的,大部分状况下,它是在代码执行前的几微秒(甚至更短)进行编译.
  • 3,JavaScript 引擎用尽了各类办法(好比 JIT,能够延 迟编译甚至实施重编译)来保证性能最佳。
  • 4,JavaScript的编译结果不能在分布式系统中进行移植。

1.2 理解做用域

1.2.1 演员表(代码编译到执行的参与者)

首先介绍将要参与到对程序 var a = 2; 进行处理的过程当中的演员们,这样才能理解接下来将要听到的对话。

  • 引擎 从头至尾负责整个 JavaScript 程序的编译及执行过程。
  • 编译器 引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。
  • 做用域 引擎的另外一位好朋友,负责收集并维护由全部声明的标识符(变量)组成的一系列查询,并实施一套很是严格的规则,肯定当前执行的代码对这些标识符的访问权限。

1.2.2 对话(代码编译执行过程)

1.2.3 做用域的LHS查询和RHS查询

由上图可知,引擎在得到编译器给的代码后,还会对做用域进行询问变量.

如今将例子改成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查询.

1.2.4 引擎和做用域的对话

这部分比较简单就是经过拟人方式比喻引擎和做用域的合做过程.一句话归纳就是,引擎进行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(..)。

1.3做用域嵌套

当一个块或函数嵌套在另外一个块或函数中时,就发生了做用域的嵌套。进而造成了一条做用域链.所以,在当前做用 域中没法找到某个变量时,引擎就会在外层嵌套的做用域中继续查找,直到找到该变量, 或抵达最外层的做用域(也就是全局做用域)为止。

当引擎须要对做用域进行查询时.引擎会从当前的执行做用域开始查找变量,若是找不到, 就向上一级继续查找。当抵达最外层的全局做用域时,不管找到仍是没找到,查找过程都 会中止。

1.4 异常

例子:

function foo(a) { 
  console.log( a + b ); 
  b = a;
}
foo( 2 );
复制代码
  • 若是 RHS 查询在全部嵌套的做用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。例如上面例子中console.log(a+b)因为RHS此时是找不到b的值.故会抛出ReferenceError.
  • 若是 RHS 查询找到了一个变量,可是你尝试对这个变量的值进行不合理的操做, 好比试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的 属性,那么引擎会抛出另一种类型的异常,叫做 TypeError
  • 当引擎执行 LHS 查询时,若是在顶层(全局做用域)中也没法找到目标变量,全局做用域中就会建立一个具备该名称的变量,并将其返还给引擎,前提是程序运行在非 “严格模式”下。例如上面例子中的b=a;.
  • 在严格模式中 LHS 查询失败时,并不会建立并返回一个全局变量,引擎会抛出同 RHS 查询 失败时相似的 ReferenceError 异常。

1.5 LHS与RHS小结

  • LHS和RHS查询都是引擎对做用域的查询
  • LHS和RHS查询都是只对变量进行查询
  • LHS和RHS都会沿着做用域链进行查询,直到最上层的全局做用域.若是没找到的话,在非严格模式下,LHS则会在全局建立一个相同名称的变量.RHS则会抛出ReferenceError的异常.
  • 若是查找的目的是对变量进行赋值,那么就会使用 LHS 查询;若是目的是获取变量的值,就会使用 RHS 查询。
  • LHS只是找到变量的容器而已,方便进行赋值
  • =操做符或调用函数时传入参数的操做都会致使关联做用域的赋值操做。此时都会进行LHS查询
  • RHS查询则须要找到变量的值.

第二章 词法做用域

做用域分为两种工做模式:

  • 1,词法做用域.是目前最为广泛的,被大多数编程语言所采用的模式.固然JavaScript也是使用的词法做用域.
  • 2,动态做用域.使用较少,好比 Bash 脚本、Perl 中的一些模式等.

2.1 词法阶段

词法阶段: 大部分标准语言编译器的第一个工做阶段叫做词法化(也叫单词化)。词法化的过程会对源代码中的字符进行检查,若是是有状态的解析过程,还会赋予单词语义。

词法做用域: 词法做用域就是定义在词法阶段的做用域也被称为静态做用域。即在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个逐级包含的"气泡做用域"。

  • 1:包含着整个全局做用域,其中只有一个标识符:foo。
  • 2:包含着 foo 所建立的做用域,其中有三个标识符:a、bar 和 b。
  • 3:包含着 bar 所建立的做用域,其中只有一个标识符:c。

注意: 没有任何函数的气泡能够(部分地)同时出如今两个外部做用域的气泡中,就如同没有任何函数能够部分地同时出如今两个父级函数中同样。

引擎对做用域的查找:
这一部分在上一节中已经说过,就是从当前做用域逐级向上,直到最上层的全局做用域.这里再进一步进行讲解.做用域查找会在找到第一个匹配的标识符时中止。在多层的嵌套做用域中能够定义同名的标识符,这叫做“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应, 做用域查找始终从运行时所处的最内部做用域开始,逐级向外或者说向上进行,直到碰见第一个匹配的标识符为止。

注意:

  • 全局变量会自动成为全局对象(好比浏览器中的 window对象)的属性,所以能够不直接经过全局对象的词法名称,而是间接地经过对全局对象属性的引 用来对其进行访问。例如:window.a 经过这种技术能够访问那些被同名变量所遮蔽的全局变量。但非全局的变量若是被遮蔽了,不管如何都没法被访问到。
  • 词法做用域查找只会查找一级标识符,好比 a、b 和 c。若是代码中引用了 foo.bar.baz,词法做用域查找只会试图查找 foo 标识符,找到这个变量后,对象属性访问规则会分别接管对 bar 和 baz 属性的访问。

2.2 欺骗词法

欺骗词法: 引擎在运行时来“修改”(也能够说欺骗)词法做用域.或者说就是在引擎运行时动态地修改词法做用域(原本在编译词法化就已经肯定的).

欺骗词法的两种机制:(下面这两种机制理解了解便可,不推荐实际开发使用)

2.2.1 eval

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”。

注意:

  • eval(..) 一般被用来执行动态建立的代码.能够据程序逻辑动态地将变量和函数以字符形式拼接在一块儿以后传递进去。
  • 在严格模式下,eval(...)没法修改所在的做用域。
  • 与eval(...)相似,setTimeout(..)和 setInterval(..) 的第一个参数能够是字符串,字符串的内容能够被解释为一段动态生成的函数代码。
  • new Function(..) 函数的行为也很相似,最后一个参数能够接受代码字符串,并将其转化为动态生成的函数(前面的参数是这个新生成的函数的形参)。这种构建函数的语法比 eval(..) 略微安全一些,但也要尽可能避免使用。
var sum = new Function("a", "b", "return a + b;");
console.log(sum(1, 1111));  //1112

复制代码

2.2.2 with(不推荐实际使用)

例子:

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(..) 也被禁止了。

2.2.3 性能

JavaScript 引擎会在编译阶段进行数项的性能优化。其中有些优化依赖于可以根据代码的词法进行静态分析,并预先肯定全部变量和函数的定义位置,才能在执行过程当中快速找到标识符。可是eval(..) 和 with会在运行时修改或建立新的做用域,以此来欺骗其余在书写时定义的词法做用域。这么作就会致使引擎没法知道eval和with它们对词法做用域进行什么样的改动.只能对部分不进行处理和优化!所以若是代码中大量使用 eval(..) 或 with,那么运行起来必定会变得很是慢!。

2.3 小结

  • 词法做用域是在你书写代码时就已经决定了的.在编译的第一阶段词法分析阶段产生词法做用域.此时词法做用域基本可以知道所有标识符在哪里以及是如何声明的,从而可以预测在执行过程当中如何对它 们进行查找。
  • eval(..) 和 with。前者能够对一段包含一个或多个声明的“代码”字符串进行演算,并借此来修改已经存在的词法做用域(在运行时)。后者本质上是经过将一个对象的引用看成做用域来处理,将对象的属性看成做用域中的标识符来处理,从而建立了一个新的词法做用域(一样是在运行时)。
  • 通常不要在实际代码中使用eval(...)和with,由于不只危险,并且会形成性能问题!

第三章 函数做用域和块做用域

3.1 函数中的做用域

  • JavaScript 具备基于函数的做用域,通常状况下每声明 一个函数都会建立一个函数做用域.
  • 函数做用域的含义是指,属于这个函数的所有变量均可以在整个函数的范围内使用及复用(事实上在嵌套的做用域中也能够使用)。这样的好处是JavaScript 变量能够根据须要改变值类型。

3.2 隐藏内部实现

由于

  • 子级函数做用域能够直接访问父级函数做用域里的标识符;
  • 父级函数做用域不能直接访问子级函数做用域里的标识符.

因此用函数声明对代码进行包装,实际上就是把这些代码“隐藏”起来了。

为何要将代码进行"隐藏"? 由于最小受权或最小暴露原则。这个原则是指在软件设计中,应该最小限度地暴露必 要内容,而将其余内容都“隐藏”起来,好比某个模块或对象的 API 设计。 隐藏的好处:

  • 实现代码私有化,减小外部对内部代码的干扰,保持其稳定性.
  • 规避冲突: 能够避免同名标识符之间的冲突, 两个标识符可能具备相同的名字但用途却不同,无心间可能形成命名冲突。冲突会致使 变量的值被意外覆盖。那么通常规避冲突的手段有哪些?
      1. 全局命名空间: 变量冲突的一个典型例子存在于全局做用域中。当程序中加载了多个第三方库时,若是它们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引起冲突。这些库一般会在全局做用域中声明一个名字足够独特的变量,一般是一个对象。这个对象被用做库的命名空间,全部须要暴露给外界的功能都会成为这个对象(命名空间)的属性,而不是将本身的标识符暴漏在顶级的词法做用域中。
    • 2.模块管理: 另一种避免冲突的办法和现代的模块机制很接近,就是从众多模块管理器中挑选一个来 使用。实际上就是咱们经常使用的amd,commonjs,import模块机制.

3.3 函数做用域

函数声明与函数表达式:

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 Expression): 将函数定义为表达式语句(一般是变量赋值,也能够是自调用形式)的一部分。经过函数表达式定义的函数能够是命名的,也能够是匿名的。由于它能够没有函数名,所以常被用做匿名函数.若是有,其函数名也只存在自身的函数做用域.而且函数表达式不能以“function”开头.函数表达式能够存储在变量或者对象属性里. (在函数声明前加上运算符是能够将其转化为函数表达式的.例如!,+,-,().举个例子:!function(){console.log(1)}()的结果是1,并不会报错)
  • 函数声明(Function Declaration):  函数声明是一种独立的结构,它会声明一个具名函数,并必须以function开头. 且函数声明会进行函数提高.使它能在其所在做用域的任意位置被调用,即后面的代码中能够将此函数经过函数名赋值给变量或者对象属性.
  • Function()构造器: 即便用Function构造器建立函数.不推荐这种用法, 容易出问题
//Function()构造器
var f =new Function()

// 函数表达式
var f = function() {
      console.log(1);  
}

// 函数声明
function f (){
     console.log(2);
}

console.log(f())
//思考一下,这里会打印出什么
复制代码

怎么区分函数声明和函数表达式: 看 function 关键字出如今声明中的位置(不只仅是一行代码,而是整个声明中的位置)。若是 function 是声明中的第一个词,那么就是一个函数声明,不然就是一个函数表达式。例如上例中,是从(开始而不是function.

补充: 上面这段是原书的解释,我以为这个解释并不彻底,这里给出我本身的解释.

  • 表象区别:和它说的同样,只要是以function开头进行声明,而且含有函数名的就必定是函数声明.
  • 内在区别:其实我在上面补充二者的定义时已经说得很清楚了,我再对比总结下.
    • 函数提高:函数声明,会将整个函数进行提高.而函数表达式则不会提高,它是在引擎运行时进行赋值,且要等到表达式赋值完成后才能调用。
    • 函数表达式是能够没有函数名的,若是有,它的函数名也只存在于自身的做用域,var f = function fun(){console.log(fun)}其余地方是没有的.这也避免了全局污染,也方便递归.

3.3.1 匿名和具名

函数表达式能够是匿名的,而函数声明则不能够省略函数名.有函数名的就是具名函数,没有函数名的就是匿名函数.

匿名函数的缺点:

    1. 匿名函数在栈追踪中不会显示出有意义的函数名,使得调试很困难。
    1. 若是没有函数名,当函数须要引用自身时只能使用已通过期的arguments.callee引用,好比在递归中。另外一个函数须要引用自身的例子,是在事件触发后事件监听器须要解绑自身。
    1. 匿名函数省略了对于代码可读性/可理解性很重要的函数名。一个描述性的名称可让代码不言自明。

因此给函数表达式指定一个函数名能够有效解决以上问题。始终给函数表达式命名是一个最佳实践.

PS: 我的意见是若是函数表达式有赋值给变量或属性名或者就是一次性调用的.实际上是不必加上函数名.由于代码里取名原本就很难,取很差反而会形成误解.

3.3.2 当即执行函数表达式

好比 (function foo(){ .. })()。第一个 ( ) 将函数变成表达式,第二个 ( ) 执行了这个函数。这就是当即执行函数表达式,也被称为IIFE,表明当即执行函数表达式 (Immediately Invoked Function Expression);

IIFE能够具名也能够匿名.好处和上面提到的同样.IIFE还能够是这种形式(function(){ .. }()).这两种形式在功能上是一致的。

3.4 块做用域

函数做用域是JavaScript最多见的做用域单元,有时咱们仅会将var赋值变量在if或for的{...}内使用,而不会在其余地方使用.但它仍然会对外层的函数做用域形成污染.这个时候就会但愿能有一个做用域能将其外部的函数做用域隔开,声明的变量仅在此做用域有效.块做用域(一般就是{...}包裹的内部)就能够帮咱们作到这点.

从 ES3 发布以来,JavaScript 中就有了块做用域,而 with 和 catch 分句就是块做用域的两个小例子。

3.4.1 with

咱们在第 2 章讨论过 with 关键字。它不只是一个难于理解的结构,同时也是块做用域的一个例子(块做用域的一种形式),用 with 从对象中建立出的做用域仅在 with 声明中而非外部做用域中有效。

3.4.2 try/catch

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以前咱们能够使用这种方法来使用块做用域.

3.4.3 let

ES6 引入了新的 let 关键字,提供了除 var 之外的另外一种变量声明方式。let 关键字能够将变量绑定到所在的任意做用域中(一般是 { .. } 内部)。

用 let 将变量附加在一个已经存在的块做用域上的行为是隐式的。例如在if的{...}内用let声明一个变量.那什么是显式地建立块做用域呢?就是单首创建{}来做为let的块做用域.而不是借用if或者for提供的{}.例如{let a=2;console.log(a)}
注意: 使用 let 进行的声明不会在块做用域中进行提高.
块做用域的好处:

  • 1,垃圾收集
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);
复制代码
    1. let循环
for (let i=0; i<10; i++) { 
	  console.log( i );
     }
console.log( i ); // ReferenceError
复制代码

for 循环头部的 let 不只将 i 绑定到了 for 循环的块中,事实上它将其从新绑定到了循环的每个迭代中,确保使用上一个循环迭代结束时的值从新进行赋值。这样就避免了i对外部函数做用域的污染.

3.4.4 const

除了 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!
复制代码

3.5 小结

函数是 JavaScript 中最多见的做用域单元。本质上,声明在一个函数内部的变量或函数会在所处的做用域中“隐藏”起来,能够有效地与外部做用域隔开.

但函数不是惟一的做用域单元。块做用域指的是变量和函数不只能够属于所处的做用域,也能够属于某个代码块(一般指 { .. } 内部)即块做用域。ES6中就提供了let和const来帮助建立块做用域.

第四章 提高

4.1 先有鸡(赋值)仍是先有蛋(声明)

考虑第一段代码

a = 2;
var a; 
console.log( a );
复制代码

输出结果是2,而不是undefined

考虑第二段代码

console.log( a ); 
var a = 2;
复制代码

输出结果是undefined,而不是ReferenceError 考虑完以上代码,你应该会考虑这个问题.究竟是声明(蛋)在前,仍是赋值(鸡)在前?

4.2 编译器再度来袭

编译器的内容,回忆一下,引擎会在解释 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;不包含有赋值操做的声明.
  • 函数声明:注意是函数声明,而不是函数表达式!(不清楚能够看前面的3.3节,我有详细说明).函数声明提高,是将整个函数进行提高,而不是仅仅函数名的提高.

4.3 函数优先

函数声明和变量声明都会被提高。可是一个值得注意的细节(这个细节能够出如今有多个“重复”声明的代码中)是函数会首先被提高,而后才是变量。 考虑如下代码:

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会忽略前面已经声明的声明(无论是变量声明仍是函数声明,只要其名称相同,则后续不会再进行重复声明).可是对该变量新的赋值,会覆盖以前的值.
一句话归纳:函数声明的优先级高于变量声明,会排在它前面.

4.4 小结

  • 对于var a = 2 JavaScript引擎会将var a和 a = 2看成两个单独的声明,第一个是编译阶段的任务,而第二个则是执行阶段的任务。
  • 论做用域中的声明出如今什么地方,都将在代码自己被执行前首先进行处理。 能够将这个过程形象地想象成全部的声明(变量和函数)都会被“移动”到各自做用域的最顶端,这个过程被称为提高。
  • 声明自己会被提高,而包括函数表达式的赋值在内的赋值操做并不会提高(即赋值操做都不会提高)。
  • 注意:,当普通的 var 声明和函数声明混合在一块儿的时候,而且声明相同时(var的变量名和函数名相同时,会引起js对重复声明的忽略)!必定要注意避免重复声明!

第五章 做用域闭包

5.1 启示

  • JavaScript中闭包无处不在,你只须要可以识别并拥抱它。
  • 闭包是基于词法做用域书写代码时所产生的天然结果,你甚至不须要为了利用它们而有意识地建立闭包。

5.2 实质问题 && 5.3 如今我懂了

由于这两小节理解透了其实发现书里也没讲什么,这里就进行合并,并补充拓展我本身的理解和总结.
什么是闭包?(广义版)
书中解释: 当函数能够记住并访问所在的词法做用域时,就产生了闭包,即便函数是在当前词法做用域以外执行。
MDN的解释: 闭包是函数和声明该函数的词法环境的组合。
个人解释(详细版): 必须包含两点:

  • 1,有函数.因为函数自身的特性,它能访问所在的词法做用域.并能保存外部词法做用域的变量和函数到本身的函数做用域.
  • 2,有该函数所在的词法环境.其实在JavaScript中任何函数都会处在一个词法环境中.无论是全局做用域仍是函数做用域.

综上简单版就是:MDN的解释闭包是函数和声明该函数的词法环境的组合。
还能够继续延伸成极简版:JavaScript中的函数就会造成闭包
Tips: 注意到上面对词法做用域词法环境两词的分开使用了吗?1,里此时函数还没被执行,因此使用的是词法做用域即静态做用域.2,里,此时函数被执行,此时词法做用域就会变成词法环境(包含静态做用域与动态做用域).因此其实MDN的解释其实更准确一点,

咱们平常使用时所说的闭包(狭义版,严格意义上的):
为了便于对闭包做用域的观察和使用.咱们实际使用时会将闭包的函数做用域暴露给当前词法做用域以外.也就是本书一直强调的闭包函数须要在它自己的词法做用域之外执行.做者认为符合这个条件才称得上是真正的闭包(也就是咱们平常使用常说的'使用闭包',而且使用任何回调函数其实也是闭包).
因此狭义版就是:闭包是函数和声明该函数的词法环境的组合,而且将闭包的函数做用域暴露给当前词法做用域以外.

闭包暴露函数做用域的三种方式:
下面部分是书中没有的,是本身实际使用时的总结,而且符合这三种形式之一的就是咱们平常使用时所说的闭包(狭义版)

  • 1,经过外部函数的参数进行暴露.
function foo() { 
  var a = 2;
  function bar() { 
   baz(a) //经过外部函数的参数进行暴露
  }
  bar(); 
};
function baz(val) { 
   console.log( val ); // 2 
}
foo();
复制代码
  • 2,经过外部做用域的变量进行暴露
var val;
function foo() { 
  var a = 2;
  function bar() { 
   val=a //经过外部做用域的变量进行暴露
  }
  bar(); 
};
foo();
console.log(val)  //2
复制代码
  • 3,经过return直接将整个函数进行暴露
function foo() { 
   var a = 2;
   function bar() { 
    console.log(a)
   }
   return bar //经过return直接将整个函数进行暴露
};
var val=foo();
val()  //2
复制代码

关于闭包的内存泄露问题:
首先必须声明一点:使用闭包并不必定会形成内存泄露,只有使用闭包不当才可能会形成内存泄露.(吐槽:面试不少新人时,张口就说闭包会形成内存泄露)
为何闭包可能会形成内存泄露呢?缘由就是上面提到的,由于它通常会暴露自身的做用域给外部使用.若是使用不当,就可能致使该内存一直被占用,没法被JS的垃圾回收机制回收.就形成了内存泄露.
注意: 即便闭包里面什么都没有,闭包仍然会隐式地引用它所在做用域里的所用变量. 正由于这个隐藏的特色,闭包常常会发生不易发现的内存泄漏问题.
常见哪些状况使用闭包会形成内存泄露:

  • 1,使用定时器未及时清除.由于计时器只有先中止才会被回收.因此决办法很简单,将定时器及时清除,并将形成内存的变量赋值为null(变成空指针)
  • 2,相互循环引用.这是常常容易犯的错误,而且也不容易发现.举个栗子:
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.

  • 3,将闭包引用到全局变量上.由于全局变量是只有当页面被关闭的时候才会被回收.
  • 4,在闭包中对DOM进行不当的引用.这个常见于老IE浏览器,现代浏览器已经长大了,已经学会了本身处理这种状况了.这里就不赘述了.想知道的能够自行问谷娘和度娘.

总而言之,解决办法就是使闭包的能正常引用,能被正常回收.若是实在不行,就是在使用完后,手动将变量赋值null,强行进行垃圾回收.

5.4 循环和闭包

看以下例子:

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,块做用域不能和函数同样有名称(函数名) 不少不方便使用闭包或者比较麻烦的时候,是能够考虑经过块做用域进行解决.

总结一下通常何时考虑使用闭包:
这部分也是本身工做使用的总结,若是有补充或者不对的地方,欢迎留言指正.

  • 1,须要建立一个独立的做用域并隐藏一些变量或函数,不被外部使用;或者想保存一些外部做用域的变量或函数到这个独立做用域.
  • 2,只想暴露一部分自身做用域的变量或函数给外部使用.

5.5 模块

首先看下面的例子:

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个条件:(这里结合上面的例子,对书中的定义进行说明方便理解)

  • 1, 必须有外部的封闭函数(即CoolModule),该函数必须至少被调用一次(每次调用都会建立一个新的模块实例-->模块实例指的就是函数return返回的对象)。
  • 2, 封闭函数(即CoolModule)必须返回至少一个内部函数(即doSomething, doAnother),这样内部函数才能在私有做用域中造成闭包,而且能够访问或者修改私有的状态(即something, another)。

模块:
表面上看由模块函数(例子中的CoolModule)所返回的对象就是模块.但模块还必须还包含模块函数的内部函数(即闭包函数).只有包含了才能真正称得上是模块.才强调一次这里的模块与模块化里的模块是有区别的,也不是nodejs里的模块.

模块函数:
模块函数也就是模块构造器,例子中的CoolModule().通常它有两个常见用法.

  • 经过接受参数,对输出的模块进行修改.
  • 经过添加模块里添加相关的内部函数,实现对输出模块数据的增删改查.(书中用命名将要做为公共API返回的对象.我以为命名应该是用错了,应该是修改即增删改查更好)

5.5.1 现代的模块机制

大多数模块依赖加载器 / 管理器本质上都是将这种模块定义封装进一个友好的 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

复制代码

这节的主要内容仍是了解如今是如何对模块进行一个规范处理.主要是两部份内容,一个是经过名称和依赖合理定义模块并储存.另外一个则是经过名称对存储的模块的调用.其实还能够再增长一个删除模块的方法.

5.5.2 将来的模块机制

ok,这节说的模块,就是咱们常说的模块化开发.而且主要提到的就是ES6里经常使用的import.没什么好说的.

5.6 小结

吐槽: 同一个函数概念在5.5这一个小节里,竟然换着花样蹦出了三个名字!一会叫模块构造器!一会叫模块函数!以及最后的包装函数!每变化一次,都得想一遍它指的是啥!真的是无力吐槽了!!!!

闭包:当函数能够记住并访问所在的词法做用域,而且函数是在当前词法做用域以外执行,这时 就产生了闭包。

模块有两个主要特征:

  • (1)为建立内部做用域而调用了一个包装函数(模块构造器的实例化,不想对频繁换名字吐槽了);
  • (2)包装函数的返回值(也就是模块)必须至少包括一个对内部函数的引用,这样就会建立涵盖整个包装函数内部做用域的闭包。

第二部分

第一章 关于this

1.1 为何要用this

由于this 提供了一种更优雅的方式来隐式“传递”一个对象(即上下文对象)引用,所以能够将 API 设计得更加简洁而且易于复用。

1.2 误解

下面两种常见的对于 this 的解释都是错误的(看看就好,就不过多解读了,以避免增长了对错误的印象)。

1.2.1 指向自身

人们很容易把 this 理解成指向函数自身.

具名函数,能够在它内部能够使用函数名来引用自身进行递归,添加属性等。(这个知识点其实在第三章提过,既然这里又提了一遍,我也再说一遍.)例如:

function foo() {
  foo.count = 4; // foo 指向它自身
}
复制代码

匿名函数若是想要调用自身则,须要使用arguments.callee不过这个属性在ES5严格模式下已经禁止了,也不建议使用.详情能够查看MDN的说明.

1.2.2 它的做用域

切记: this 在任何状况下都不指向函数的词法做用域。你不能使用 this 来引用一个词法做用域内部的东西。 这部分只需记住这一段话就行.

终极疑问: JavaScript里的做用域究竟是对象吗? 这小节最令我在乎的是里面这句话"在 JavaScript 内部,做用域确实和对象相似,可见的标识符都是它的属性。可是做用域“对象”没法经过 JavaScript代码访问,它存在于JavaScript 引擎内部。"它让我想起了最开始学JS的一个疑问,JavaScript里的做用域究竟是对象吗.虽然"在JS里万物皆对象".可是做用域给人的感受却不像是一个对象.更像是一个范围,由函数的{}围城的范围,限制了其中变量的访问.但直觉告诉我它和对象仍是应该有点联系的.直到读到书中的这段话,更加印证了个人感受. 在JavaScript里,做用域实际上是一个比较特殊的对象,做用域里全部可见的标识符都是它的属性.只是做用域对象并不能经过JavaScript代码被咱们访问,它只存在于JavaScript引擎内部.因此做用域做为一个"对象"是常常被咱们忽略.

1.3 this究竟是什么

this 是在运行时(runtime)进行绑定的,并非在编写时绑定,它的上下文(对象)取决于函数调用时的各类条件。this 的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

当一个函数被调用时,会建立一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this 就是记录的其中一个属性,会在函数执行的过程当中用到。(PS:因此this并不等价于执行上下文)

1.4 小结

  • 学习 this 的第一步是明白 this 既不指向函数自身也不指向函数的词法做用域
  • this 其实是在函数被调用时发生的绑定,它指向什么彻底取决于函数在哪里被调用(关于this你必须记住的话)

第二章 this全面解析

2.1 调用位置

经过上节咱们知道,this的绑定与函数的调用位置有关.那调用位置是什么.调用位置就是函数在代码中被调用的位置(而不是声明的位置)。

要寻找调用位置,最重要的是要分析调用栈(就是为了到达当前执行位置所调用的全部函数)。咱们关心的调用位置就在当前正在执行的函数的前一个调用中。PS:调用栈实际上是一个解释起来有点复杂的概念.这里我就不过多解释,这里推荐一篇文章,解释得不错.

这节书里的例子解释得不错,这里就不复制代码了.其实分析调用栈只是为了在运行时找到咱们关心的函数到底在哪里和被谁调用了. 可是实际别写代码时,其实并不会分析得这么清楚的,咱们仍是只需记住this的指向就是咱们调用该函数的上下文对象.意思就是咱们在哪里调用该函数,this就指向哪里.而且查看调用栈还能够经过浏览器的开发者工具,只需在疑惑的代码上一行加上debugger便可.浏览器在调试模式时,咱们就能够在调用列表里查看调用栈.咱们通常也仅在查找bug时,会使用该方法.

2.2 绑定规则

在找到调用位置后,则须要断定代码属于下面四种绑定规则中的哪种.而后才能对this进行绑定.
注意: this绑定的是上下文对象,并非函数自身也不是函数的词法做用域

2.2.1 默认绑定

什么是独立函数调用:对函数直接使用而不带任何修饰的函数引用进行调用.简单点一个函数直接是func()这样调用,前面什么都没有.不一样于经过对象属性调用例如obj.func(),也没有经过new关键字new Function();也没有经过apply,bind,call强制改变this指向.
默认绑定: 当被用做独立函数调用时(不论这个函数在哪被调用,无论全局仍是其余函数内),this默认指向到window;
注意: 若是使用严格模式(strict mode),那么全局对象将没法使用默认绑定,所以 this 会绑定到 undefined.

2.2.2 隐式绑定

隐式绑定: 函数被某个对象拥有或者包含.也就是函数被做为对象的属性所引用.例如obj.func().此时this会绑定到该对象上.
隐式丢失: 无论是经过函数别名或是将函数做为入参形成的隐式丢失.只需找到它真正的调用位置,而且函数前没有任何修饰也没有显式绑定(下节会讲到)(非严格模式下).那么this则会进行默认绑定,指向window.
注意: 实际工做中,大部分this使用错误都是由对隐式丢失的不理解形成的.记住函数调用前没有任何修饰和显式绑定(其实就是call、apply、bind),this就指向window

2.2.3 显式绑定

在分析隐式绑定时,咱们必须在一个对象内部包含一个指向函数的属性,并经过这个属性间接引用函数,从而把 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。

2.2.4 new绑定

JavaScript 中 new 的机制实际上和面向类的语言彻底不一样。在 JavaScript 中,构造函数只是一些 使用 new 操做符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上, 它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操做符调用的普通函数而已。实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用”。

使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操做。

  • 1,建立(或者说构造)一个全新的对象。
  • 2,这个新对象会被执行[[原型]]链接。
  • 3,这个新对象会绑定到函数调用的this。
  • 4,若是函数没有返回其余对象,那么new表达式中的函数调用会自动返回这个新对象。

示例:

function foo(a) { 
  this.a = a;
}
var bar = new foo(2); 
console.log( bar.a ); // 2
复制代码

使用 new 来调用 foo(..) 时,咱们会构造一个新对象并把它绑定到 foo(..) 调用中的 this 上。
说明:对于上面这句话进行解释下,若是在一个函数前面带上 new 关键字来调用, 那么背地里将会建立一个链接到该函数的 prototype 的新对象,this就指向这个新对象;

2.3 优先级

直接上结论:
new绑定=显示绑定>隐式绑定>默认绑定
说明: new绑定与显示绑定是不能直接进行测试比较,但经过分析发现new绑定内部实际上是使用了硬绑定(显示绑定的一种),因此new绑定和显示绑定优先级应该差很少.但话说回来,通常实际使用时,不会这种复杂的交错绑定.因此只需记住下面的断定便可.

判断this:
如今咱们能够根据优先级来判断函数在某个调用位置应用的是哪条规则。能够按照下面的顺序来进行判断:

  • 1,函数是否在new中调用(new绑定)?若是是的话this绑定的是新建立的对象。 var bar = new foo()
  • 2,函数是否经过call、apply(显式绑定)或者硬绑定调用?若是是的话,this绑定的是 指定的对象。var bar = foo.call(obj2)
  • 3,函数是否在某个上下文对象中调用(隐式绑定)?若是是的话,this 绑定的是那个上 下文对象。var bar = obj1.foo()
  • 4,若是都不是的话,使用默认绑定。若是在严格模式下,就绑定到undefined,不然绑定到 全局对象。var bar = foo() 就是这样。对于正常的函数调用来讲,理解了这些知识你就能够明白 this 的绑定原理了。

2.4 绑定例外

2.4.1 被忽略的this

若是你把 null 或者 undefined 做为 this 的绑定对象传入 call、apply 或者 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则,this会绑定到window上.
使用情景:
一种很是常见的作法是使用 apply(..) 来“展开”一个数组(也能够用来方便地参数注入),并看成参数传入一个函数。相似地,bind(..) 能够对参数进行柯里化(预先设置一些参数).经过自带bind方法实现柯里化是很方便的,比本身写要简化好多.

注意:

  • 在 ES6 中,能够用 ... 操做符代替 apply(..) 来“展 开”数组,foo(...[1,2]) 和 foo(1,2)是同样的,这样能够避免没必要要的 this 绑定。惋惜,在 ES6 中没有柯里化的相关语法,所以仍是须要使用 bind(..)。
  • 当使用null或者undefined进行绑定时,要确保该函数内没有使用this,不然此时很容易对全局变量形成破坏!尤为是使用第三方库的方法!

更安全的this
若是函数内使用了this,直接使用null则可能会对全局形成破坏.所以咱们能够经过建立一个“DMZ”(demilitarized zone,非军事区)对象——它就是一个空的非委托的对象(委托在第 5 章和第 6 章介绍)。让this绑定到这个"DMZ上.这样就不会对全局形成破坏. 怎么建立DMZ呢.就是经过Object.create(null) 建立一个空对象.这种方法和 {} 很像,可是并不会建立 Object.prototype 这个委托,因此它比 {}“更空”更加安全.

PS:实际使用通常不会遇到这种状况(也多是我太菜,没遇到),若是函数内有this,那确定是有须要调用的变量或函数,直接把它绑定到一个空对象上.那什么都取不到,还有什么意义?因此函数没有this就传入null.若是有this就把它绑定到真正须要它的对象上,而不是一个空对象上.这些是我本身的看法,若是有不妥的,欢迎留言指正.

2.4.2 间接引用

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节就应该说了!)

2.4.3 软绑定

硬绑定会大大下降函数的灵活性,使用硬绑定以后就没法使用隐式绑定或者显式绑定来修改 this。这时候则须要使用软绑定.
Tips: 这里给的软绑定方法仍是挺好的.可是建议仍是在本身的代码里使用,并注释清除.以避免别人使用,对this错误的判断.

2.5 this词法

ES6 中介绍了一种没法使用上面四条规则的特殊函数类型:箭头函数。
箭头函数不使用 this 的四种标准规则,而是根据外层(函数或者全局)做用域来决定 this。(而传统的this与函数做用域没有任何关系,它只与调用位置的上下文对象有关.这点在本章开头就已经反复强调了.)

重要:

  • 箭头函数最经常使用于回调函数中,例如事件处理器或者定时器.
  • 箭头函数能够像 bind(..) 同样确保函数的 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的编码规范建议:

    1. 只使用词法做用域并彻底抛弃错误this风格的代码;
    1. 彻底采用 this 风格,在必要时使用 bind(..),尽可能避免使用 self = this 和箭头函数。

在本身实际工做中,实际上是两种混用的,绝大部分状况下都会使用词法做用域风格.由于有时候你真的很难作到彻底统一.我如今的习惯是,在写任何函数时,开头第一个就是var me =this;这样在看到函数第一眼,就知道:哦,这个函数是用词法做用域风格的.尤为函数内涉及到回调.这样就避免了写着写着发现this绑定到其余地方去了,一个函数里面this不统一的状况.

2.6 小结

(这里总结得很好,我就所有copy了) 若是要判断一个运行中函数的 this 绑定,就须要找到这个函数的直接调用位置。找到以后就能够顺序应用下面这四条规则来判断 this 的绑定对象。

    1. 由new调用?绑定到新建立的对象。
    1. 由call或者apply(或者bind)调用?绑定到指定的对象。
    1. 由上下文对象调用?绑定到那个上下文对象。
    1. 默认:在严格模式下绑定到undefined,不然绑定到全局对象。

必定要注意,有些调用可能在无心中使用默认绑定规则。若是想“更安全”地忽略 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 这里再补充说明 上下文(对象)与函数做用域的区别于联系:

  • 上下文: 能够理解为一个对象,全部的变量都储存在里面.上下文环境是在函数被调用并被引擎执行时建立的.若是你没调用,那么就没有上下文.
  • 做用域: 除了全局做用域,只有函数和ES6新增的let,const才能建立做用域.建立一个函数就建立了一个做用域,不管你调用不调用,函数只要建立了,它就有独立的做用域.做用域控制着被调用函数中的变量访问.
  • 二者: 做用域是基于函数的,而上下文是基于对象的。做用域涉及到所被调用函数中的变量访问,而且不一样的调用场景是不同的。上下文始终是this关键字有关, 它控制着this的引用。一个做用域下可能包含多个上下文。有可能历来没有过上下文(函数没有被调用);有可能有过,如今函数被调用完毕后,上下文环境被销毁了(垃圾回收);有可能同时存在一个或多个(闭包)。

第三章 对象

3.1 语法

对象能够经过两种形式定义:声明(文字)形式(就是常说的对象字面量)和构造形式。

  • 声明形式(对象字面量):
var myObj = { 
  key: value
  // ... 
};
复制代码
  • 构造形式:
var myObj = new Object(); 
myObj.key = value;
复制代码

构造形式和文字形式生成的对象是同样的。惟一的区别是,在文字声明中你能够添加多个 键 / 值对,可是在构造形式中你必须逐个添加属性。 PS:其实咱们绝大部分状况下都是使用对象字面量形式建立对象.

3.2 类型

在JavaScript中一共有6中主要类型(术语是"语言类型")

  • string
  • number
  • boolean
  • null
  • undefined
  • object

简单数据类型: 其中string、boolean、number、null 和 undefined属于简单基本类型,并不属于对象. null 有时会被看成一种对象类型,可是这其实只是语言自己的一个 bug,即对 null 执行typeof null 时会返回字符串 "object"。实际上,null 自己是基本类型。
PS: 缘由是这样的,不一样的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判 断为 object 类型,null 的二进制表示是全 0,天然前三位也是 0,因此执行 typeof 时会返回“object”。

对象:
对象除了咱们本身手动建立的,JavaScript其实内置了不少对象,也能够说是对象的一个子类型.
内置对象:

  • String
  • Number
  • Boolean
  • Object
  • Function
  • Array
  • Date
  • RegExp
  • Error

在 JavaScript 中,这些内置对象实际上只是一些内置函数。这些内置函数能够看成构造函数(由 new 产生的函数调用——参见第 2 章)来使用.
几点说明:

  • 函数就是对象的一个子类型(从技术角度来讲就是“可调用的对象”)。JavaScript 中的函数是“一等公民”,由于它们本质上和普通的对象同样(只是能够调用),因此能够像操做其余对象同样操做函数(好比看成另外一个函数的参数)。
  • 经过字面量形式建立字符串,数字,布尔时,引擎会自动把字面量转换成 String 对象,Number对象,Boolean对象,因此它们是能够访对应对象内置的问属性和方法。
  • null 和 undefined 没有对应的构造形式,它们只有文字形式。相反,Date 只有构造,没有文字形式。
  • 对于 Object、Array、Function 和 RegExp(正则表达式)来讲,不管使用文字形式仍是构造形式,它们都是对象,不是字面量(这是确定的,由于无论哪一种形式一建立出来就是对象类型,不多是其余类型,其实是不存在字面量这一说的)。可是使用构造形式能够提供一些额外选项(内置)。
  • Error 对象不多在代码中显式建立,通常是在抛出异常时被自动建立。也能够使用 new Error(..) 这种构造形式来建立,不过通常来讲用不着。

3.3 内容

对象属性:由一些存储在特定命名位置的(任意类型的)值. 属性名:存储在对象容器内部的属性的名称.属性值并不会存在对象内.而是经过属性名(就像指针,从技术角度来讲就是引用)来指向这些值真正的存储位置(就像房门号同样).
属性名的两种形式:

    1. 使用.操做符.也是咱们最经常使用的形式.它一般被称为"属性访问". . 操做符会要求属性名知足标识符的命名规范.
    1. 使用[".."]语法进行访问.这个一般被称为"键访问".[".."]语法能够接受任意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一样符合上一章所讲的规则
//结果是否和你所想得同样呢?
复制代码

3.3.1 可计算属性名

ES6 增长了可计算属性名,能够在文字形式中使用 [] 包裹一个表达式来看成属性名:

var prefix = "foo";

var myObject = {
   [prefix + "bar"]:"hello", 
   [prefix + "baz"]: "world"
};
myObject["foobar"]; // hello
myObject["foobaz"]; // world
复制代码

3.3.2 属性与方法

  • 咱们常常把对象内部引用的函数称为“方法”(的确如此).
  • 实际上函数并不属于该对象,它不过是对函数的引用罢了.对象属性访问返回的函数和其余函数没有任何区别(除了可能发生的隐式绑定this到该对象)。
  • 即便你在对象的文字形式中声明一个函数表达式,这个函数也不会“属于”这个对象—— 它们只是对于相同函数对象的多个引用。

3.3.3 数组

  • 数组支持[]形式访问储存的值,其中[]内的值默认形式为数值下标(为从0开始的整数,也就是常说的索引).例如myArray[0]
  • 数组也是对象,因此虽然每一个下标都是整数,你仍然能够给数组添加属性.例如myArray.baz = "baz".注意:添加新属性后,虽然能够访问,但数组的 length 值不会改变.
  • 数组能够经过myArray[1]=11;myArray["2"]=22;这种形式对数组内容进行修改,添加.
  • 虽然数组也能够和对象同样经过键/值 对 形式来使用.但JS已经对数组的行为和用途进行了优化.因此仍是建议使用默认的下标/值 对 形式来使用.

3.3.4 复制对象

  • 复制分为浅拷贝和深拷贝.浅拷贝会对对象中的基本数据类型进行复制(在内存中开辟新的区域),对于对象则是继续引用.而不是从新建立一个"同样的"对象.深拷贝则是对其中的全部内(容包括对象)进行深层次的复制.
  • 通常状况下咱们能够经过JSON来复制对象.var newObj = JSON.parse( JSON.stringify( someObj ) );.但须要指出的是这种方法对于包含function函数或者Date类型的对象则无论用!
  • ES6 定义了 Object.assign(..) 方法来实现浅复制。具体用法在这就不赘述了.

3.3.5 属性描述符

从 ES5 开始,全部的属性都具有了属性描述符。

  • 查看属性描述符: 能够使用Object.getOwnPropertyDescriptor( myObject, "a" );方法查看myObject对象里属性a的属性描述符.
  • 配置属性描述符: 能够使用Object.defineProperty(..)方法对属性的属性描述符就像配置.举个例子:
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。下面分别介绍剩下的三个属性描述符键值:

  • 1. Writable 决定是否能够修改属性的值。当被设置为false后,再对属性值进行修改,则会静默失败(silently failed,修改不成功,也不报错)了。若是在严格模式下,则会报出TypeError错误.
  • 2. Configurable 决定属性描述符是否可配置.若是为true,就能够使用 defineProperty(..) 方法来修改属性描述符.注意:无论是否是处于严格模式,修改一个不可配置的属性描述符都会出错。而且把 configurable 修改为 false 是单向操做,没法撤销! 可是有个例外即使属性是 configurable:false,咱们仍是能够 把 writable 的状态由 true 改成 false,可是没法由 false 改成 true。除了没法修改,configurable:false 还会禁止删除这个属性.
  • 3. Enumerable 决定该属性是否会出如今对象的属性枚举中.好比说 for..in 循环。若是把 enumerable 设置成 false,这个属性就不会出如今枚举中,虽然仍然能够正常访问它。相对地,设置成 true 就会让它出如今枚举中。

3.3.6 不变性

除了上面提到的Object.defineProperty(..),ES5还能够经过不少种方法来实现属性或者对象的不可变.
注意: 这些全部方法都是只能浅不变,若是目标对象引用了其余对象(数组、对象、函数,等),其余对象的内容不受影响,仍然是可变的.相似于浅拷贝.

说明: 在 JavaScript 程序中不多须要深不可变性。 有些特殊状况可能须要这样作,可是根据通用的设计模式,若是你发现须要密封或者冻结全部的对象,那你或许应当退一步,从新思考一下程序的设计,让它能更好地应对对象值的改变。

方法:

  • 1. 对象常量(不可改) 结合 writable:false 和 configurable:false 就能够建立一个真正的常量属性(不可修改、重定义或者删除)
  • 2. 禁止扩展(不可增) 使用 Object.prevent Extensions(myObject),能够禁止一个对象添加新属性而且保留已有属性.在非严格模式下,建立属性 b 会静默失败。在严格模式下,将会抛出 TypeError 错误。
  • 3. 密封(不可配置,但可修改) 使用Object.seal(..) 会建立一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..) 并把全部现有属性标记为 configurable:false。密封以后不只不能添加新属性,也不能从新配置或者删除任何现有属性(虽然能够修改属性的值)。
  • 4. 冻结(不可配置,也不可修改) Object.freeze(..) 会建立一个冻结对象,这个方法实际上会在一个现有对象上调用 Object.seal(..) 并把全部“数据访问”属性标记为 writable:false,这样就没法修改它们的值。这个方法是你能够应用在对象上的级别最高的不可变性,它会禁止对于对象自己及其任意直接属性的修改(不过就像咱们以前说过的,这个对象引用的其余对象是不受影响的)。

注意: 你能够“深度冻结”一个对象(连引用的对象也冻结),具体方法为,首先在这个对象上调用 Object.freeze(..), 而后遍历它引用的全部对象并在这些对象上调用 Object.freeze(..)。可是必定要谨慎!由于你引用的对象可能会在其余地发也被引用.

说明: 在 JavaScript 程序中不多须要深不可变性。有些特殊状况可能须要这样作, 可是根据通用的设计模式,若是你发现须要密封或者冻结全部的对象,那你或许应当退一步,从新思考一下程序的设计,让它能更好地应对对象值的改变。

3.3.7 [[Get]]

var myObject = { 
   a: 2
};
myObject.a; // 2
复制代码

myObject.a是怎么取到值2的?
myObject.a 经过对象默认内置的[[Get]] 操做(有点像函数调用:[Get]).首先它会在对象中查找是否有名称相同的属性, 若是找到就会返回这个属性的值。若是没有找到名称相同的属性,按照 [[Get]] 算法的定义会执行另一种很是重要的行为。其实就是遍历可能存在的 [[Prototype]] 链,也就是在原型链上寻找该属性。若是仍然都没有找到名称相同的属性,那 [[Get]] 操做会返回值 undefined.

注意: 若是你引用了一个当前词法做用域中不存在的变量,并不会像对象属性同样返回 undefined,而是会抛出一个 ReferenceError 异常.

3.3.8 [[Put]]

既然有能够获取属性值的 [[Get]] 操做,就必定有对应的 [[Put]] 来设置或者建立属性.

[[Put]] 被触发时的操做分为两个状况:1. 对象中已经存在这个属性 2. 对象中不存在这个属性.

若是对象中已经存在这个属性,[[Put]] 算法大体会检查下面这些内容:

    1. 属性是不是访问描述符(参见下一节)?若是是而且存在setter就调用setter。
    1. 属性的数据描述符中writable是不是false?若是是,在非严格模式下静默失败,在严格模式下抛出 TypeError 异常。
    1. 若是都不是,将该值设置为属性的值。

若是对象中不存在这个属性,[[Put]] 操做会更加复杂。会在第 5 章讨论 [[Prototype]] 时详细进行介绍。

3.3.9 Getter和Setter

对象默认的 [[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操做符删除
复制代码

3.3.10 存在性

属性存在性: 如何判断一个对象是否存在某个属性(准确来讲是检查这个属性名是否存在),这时就须要用到:

    1. in操做符 in 操做符会检查属性是否在对象及其 [[Prototype]] 原型链中(参见第 5 章)。
    1. hasOwnProperty(..) hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。

注意:

  • 1.若是有的对象可能没有链接到 Object.prototype( 经过Object. create(null) 来建立——参见第 5 章)。在这种状况下,形如myObejct.hasOwnProperty(..) 就会失败。这时能够使用一种更增强硬的方法来进行判断:Object.prototype.hasOwnProperty. call(myObject,"a"),它借用基础的 hasOwnProperty(..) 方法并把它显式绑定(参见第2章)到 myObject 上。
  • 2.对于数组来讲,不要使用in操做符,由于它检查的是属性名,在数组中属性名就是索引,它并非咱们所关注的重点.对于数组咱们更关注的是它所存的值,因此对于数组检查某个值是否存在仍是采用indexOf方法.

属性可枚举性: 若是一个属性存在,且它的enumerable 属性描述符为true时.则它是可枚举的.而且能够被for..in 循环. 一个属性不只仅须要存在,还须要它的enumerable 为true才是可枚举的,才能被for...in遍历到.
注意: for...in不适合对数组进行遍历,对数组的遍历仍是使用传统的for循环.

对属性的可枚举性判断,则须要用到如下几种方法:

    1. propertyIsEnumerable(..) 会检查给定的属性名是否直接存在于对象中(而不是在原型链上)而且知足 enumerable:true。
    1. Object.keys(..) 会返回一个数组,包含全部可枚举属性.
    1. Object.getOwnPropertyNames(..)会返回一个数组,包含全部属性,不管它们是否可枚举。

3.4 遍历

关于这节我以为仍是以理清for..in和for..of为主.后面延伸的@@iterator及Symbol.iterator的使用,不必过于深究.注意书中123页第二行done 是一个布尔值,表示是否还有能够遍历的值。有个错误,应该改为done 是一个布尔值,表示遍历是否结束。不然你在看后面它的说明时会感受到自相矛盾.这里我也是以for..in和for..of为主进行说明,也更贴近咱们实际使用.

for..in

  • for..in 循环能够用来遍历对象的可枚举属性列表(包括 [[Prototype]] 链)。
  • 实际上for..in遍历的并非属性值,而是属性名(即键名 key).因此你想获取属性值仍是须要手动使用obj[key]来获取.
  • 通常在遍历对象时,推荐使用for..in.固然数组也是能够使用for..in的.在遍历数组时,推荐仍是使用for..of.

for..of

  • ES6 增长了一种用来遍历数组的 for..of 循环语法(若是对象自己定义了迭代器的话也能够遍历对象)
  • for..of与for..in最大的不一样点是,它循环的是属性值,而不是属性名.不过它只循环数组里存放的值,不会涉及到对象里的key.(关于这个我后面的例子里会说具体对比明的)
  • for..of 循环首先会向被访问对象请求一个迭代器对象,而后经过调用迭代器对象的next() 方法来遍历全部返回值。数组有内置的 @@iterator,(对象没有,因此不能使用for..of,除非咱们本身定义一个)所以 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]);
}
复制代码

3.5 小结

书中小结总结得挺全的,这里我就搬运下

  • JavaScript 中的对象有字面形式(好比 var a = { .. })和构造形式(好比 var a = new Array(..))。字面形式更经常使用,不过有时候构造形式能够提供更多选项。
  • 对象是 6 个(或者是 7 个,取决于你的观点)基础类型之一。对象有包括 function 在内的子类型,不一样子类型具备不一样的行为,好比内部标签 [object Array] 表示这是对象的子类型数组。
  • 对象就是键 / 值对的集合。能够经过 .propName 或者 ["propName"] 语法来获取属性值。访问属性时,引擎实际上会调用内部的默认 [[Get]] 操做(在设置属性值时是 [[Put]]), [[Get]] 操做会检查对象自己是否包含这个属性,若是没找到的话它还会查找 [[Prototype]] 链(参见第 5 章)。
  • 属性的特性能够经过属性描述符来控制,好比 writable 和 configurable。此外,能够使用 Object.preventExtensions(..)、Object.seal(..) 和 Object.freeze(..) 来设置对象(及其属性)的不可变性级别。
  • 属性不必定包含值——它们多是具有 getter/setter 的“访问描述符”。此外,属性能够是可枚举或者不可枚举的,这决定了它们是否会出如今 for..in 循环中。
  • 能够使用 ES6 的 for..of 语法来遍历数据结构(数组、对象,等等)中的值,for..of 会寻找内置或者自定义的 @@iterator 对象并调用它的 next() 方法来遍历数据值。

第四章 混合对象"类"

注意: 正如书中提示的那样,整章一半以上几乎都是讲面向对象和类的概念.会读得人云里雾里,给人哦,也许大概就是这样子的感受.后面我仍是会对那些抽象的概念找到在JavaScript里对应的"立足点",不至于对这些概念太"飘".

4.1 类理论

说明:

  • 类其是描述了一种代码的组织结构形式.
  • 在js中类常见的就是构造函数,也能够是经过ES6提供的class关键字;继承就是函数;实例化就是对象,常见的就是经过new构造函数实现的.

注意: Javascript语言不支持“类”,所谓的"类"也是模拟出的“类”。即便是ES6引入的"类"实质上也是 JavaScript 现有的基于原型的继承的语法糖。

4.1.1 “类”设计模式

一句话:类其实也是一种设计模式!

  • 类并非必须的编程基础,而是一种可选的代码抽象.
  • 有些语言(好比 Java)并不会给你选择的机会,类并非可选的——万物皆是类。
  • 其余语言(好比 C/C++ 或者 PHP)会提供过程化和面向类这两种语法,开发者能够选择其中一种风格或者混用两种风格。

4.1.2 JavaScript中的“类”

JavaScript 只有一些近似类的语法元素 (好比 new 和 instanceof),不过在后来的 ES6 中新增了一些元素,好比 class 关键字,其实质上也是 JavaScript 现有的基于原型的继承的语法糖。也不是真正的类.

4.2 类的机制

这部分书中的描述,我理解起来也比较费劲,主要是它提到的栈,堆与我理解中内存里的栈,堆相冲突了.这里简单说下个人理解,若有误,感激指正.

stack类实际上是一种数据结构.它能够储存数据,并提供一些公用的方法(这和上面提到的类很类似).可是stack类其实只是一个抽象的表示,你想对它进行操做,就须要先对它进行实例化.

4.2.1 建造

这节主要就是说明"类"和"实例"的关系. 在JavaScript里"类"主要是构造函数,"实例"就是对象.

一个类就像一张蓝图。为了得到真正能够交互的对象,咱们必须按照类来实例化一个东西,这个东西(对象)一般被称为实例,有须要的话,咱们能够直接在实例上调用方法并访问其全部公有数据属性。

总而言之:类经过实例化获得实例对象.

4.2.2 构造函数

  • 类实例是由一个特殊的类方法构造的,这个方法名一般和类名相同,被称为构造函数。
  • 实例就是由构造函数实例化的: new 构造函数.
  • 构造函数大多须要用 new 来调,这样语言引擎才知道你想要构造一个新的类实例。
  • 构造函数会返回一个对象,这个对象就是实例.这个对象能够调用类的方法.

4.3 类的继承

在面向类的语言中,你能够先定义一个类,而后定义一个继承前者的类。后者一般被称为“子类”,前者一般被称为“父类”。子类能够继承父类的行为,而且能够根据本身的需求,修改继承的行为(通常并不会修改父类的行为).注意:咱们讨论的父类和子类并非实例,在JavaScript里类通常都是构造函数。

4.3.1 多态

大概你看了它的"解释",对多态仍是懵懵懂懂.这里我再解释下:
什么是多态?
同一个操做,做用于不一样的对象,会产生不一样的结果。发出一个相同的指令后,不一样的对象会对这个指令有不一样的反应,故称为多态。 说明: 书中例子中的inherited其实就是至关于super.而且注意书中的这些例子都是伪代码! 并非真的在JavaScript里就是这样实现的.补充:这里是关于super的mdn连接.

  • 多态:
    • 相对性: 其实相对性就是子类相对于父类的引用(例如使用super实现引用),而且子类对父类的引用并不会对父类的行为形成任何影响(并不会对父类自身的行为进行从新定义),例如书中例子子类对drive()的引用.
    • 可重复定义: 子类继承父类的某个方法,并能够对这个方法进行再次定义,例如书中子类对drive()中的output进行修改.当调用方法时会自动选择合适的定义,这句话怎么理解,当子类实例化后,执行drive()方法时,它并不会直接去执行父类的drive().而是子类上的drive().简单来讲就是实例来源于那个类,它就使用那个类的方法.

说明:

  • 在 JavaScript 中“类”是属于构造函数的(相似 Foo.prototype... 这样的类型引用)。因为 JavaScript中父类和子类的关系只存在于二者构造函数对应的 .prototype 对象中,所以它们的构造函数之间并不存在直接联系,从而没法简单地实现二者的相对引用(在 ES6 的类中能够经过 super来“解决”这个问题,参见附录 A)。
  • 多态并不表示子类和父类有关联,子类获得的只是父类的一份副本。类的继承其实就是复制。
  • 其实这里讨论的这些概念其实在咱们实际工做中,已经使用了无数次,只是如今你须要理解"原来你是叫这个名字啊!"

4.3.2 多重继承

多重继承: 一个子类能够继承来自多个父类的方法.
多重继承引起的问题: 多重继承可能会出现,多个父类中方法名冲突的问题,这样子类到底引用哪一个方法?
多重继承与JavaScript: JavaScript自己并无提供多重继承功能.但它能够经过其余方法来达到多重继承的效果.

4.4 混入

JavaScript 中只有对象,并不存在能够被实例化的“类”。一个对象并不会被复制到其余对象,它们会被关联起来(参见第 5 章)(其实就是引用,因此它的多态是"相对"的)。 因为在其余语言中类表现出来的都是复制行为,所以 JavaScript 开发者也想出了一个方法来模拟类的复制行为,这个方法就是混入(就是经过混入来模拟实现类的多重继承)。

4.4.1 显式混入

郑重提醒: 书中这里的类都是对象形式的.例子里的sourceObj, targetObj,这就可能形成一个"误导",在JavaScript里是没有真正的类,所谓的类也不过是咱们模拟出来的"类",不过是一种语法糖(包括ES6里的class).在JavaScript里"所谓的类"常常是一个构造函数,你并不能这样进行遍历,只能对它的实例对象进行这种操做.不要被书中例子带进去了,不要混淆,毕竟咱们最终使用的是JavaScript(而不是其余面向对象的语言.),它里面的类经常并非一个对象!

显式混入: 书中没有给出明确的显式混入的定义,可是读完整章.基本就知道什么是显式混入了.显式混入就是经过相似mixin()方法,显式地将父对象属性逐一复制,或者有选择地复制(即例子中的存在性检查)到子对象上.

显式混入经常使用方法: 就是书中的例子, 首先有子对象,并对其进行特殊化(定义本身的属性或方法).而后再经过mixin()方法将父对象有选择地复制(即存在性检查,过滤子对象已有的属性,避免冲突)到子对象上.

显式混入注意点: 显式混入时,切记一点你要避免父对象的属性与子对象特殊化的属性冲突.这就是为何例子中要进行存在性检查,以及后面要说的混合复制,可能存在的重写风险.

1. 再说多态(其实说的就是js里的多态)
显式多态: 将父对象里的方法经过显式绑定到子对象上.就是显式多态.例如书中的例子:Vehicle.drive.call( this )。显式多态也是为了JS来模拟实现多重继承的!
说明: 在ES6以前是没有相对多态的机制。因此就使用call这种进行显式绑定实现显式动态.注意JavaScript里实现多态的方法也被称为"伪多态".因此不要对后面忽然冒出的伪多态概念而一脸懵逼(其实整本书常常作这样的事)

显式多态(伪多态)的缺陷: 由于在JavaScript 中存在屏蔽(实际是函数引用的上下文不一样),因此在引用的时候就须要使用显式伪多态的方法建立一个函数关联. 这些都会增长代码的复杂度和维护难度(过多的this绑定,真的会让代码很难读)。

2. 混合复制(显式混入另外一种不经常使用方法)
前面的显式混入的方法是先有子对象并进行特殊化,而后再有选择地复制父对象属性.这个不经常使用的方法则是反过来的,结合书中例子,它先用一个空对象彻底复制父对象的属性,而后该对象复制特殊化对象的属性,最后获得子对象.这种方法明显是比第一种麻烦的,而且在复制特殊化对象时,可能会对以前重名的属性(即复制获得的父对象属性)进行重写覆盖.因此这种方法是存在风险,且效率低下的.

显式混入的缺陷:

    1. 没法作到真正的复制: 若是复制的对象中存在对函数的引用,那么子对象获得的是和父对象同样的,对同一个函数的引用.若是某个子对象对函数进行了修改,那么父对象及其余子对象都会受到影响.很明显这是不安全的.缘由是JavaScript 中的函数没法进行真正地复制,你只能复制对共享函数对象的引用.
    1. 函数名和属性名同名: 若是混入多个对象,则可能会出现这种状况.目前如今仍没有比较好的方式来处理函数和属性的同名问题(提问:这种状况下谁的优先级更高?滑稽脸)。

3. 寄生继承
显式混入模式的一种变体被称为“寄生继承”,它既是显式的又是隐式的. 首先会复制一份父类(对象)的定义,而后混入子类(对象)的定义(若是须要的话保留到父类的特殊引用),而后用这个复合对象构建实例。
说明: 寄生继承与混合复制是很类似的,最大的区别是寄生继承是经过实例化构造函数(JS中的"类")来实现复制的.

4.4.2 隐式混入

隐式混入: 它与显示混入最大的区别,就是它没有明显的对父类(对象)属性进行复制的过程.它是经过在构造函数调用或者方法调用中使用显式绑定例如: Something.cool.call( this)来实现混入(多重继承).其本质就是经过改变this指向来实现混入.

4.5 小结

整章的重点其实就是让你理解什么叫类.除了最后一小节的混入和JavaScript有那么一点点关系.其他的小结和JavaScript都没什么关系.重要的是理解类这种思想和设计模式.
重点:

  • 1.类意味着复制!
    1. 传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中。
    1. 多态(在继承链的不一样层次名称相同可是功能不一样的函数)看起来彷佛是从子类引用父类,可是本质上引用的实际上是复制的结果。
    1. JavaScript 并不会(像类那样)自动建立对象的副本。(你只能本身手动复制,并且复制的还不完全!)
    1. 混入模式(不管显式仍是隐式)能够用来模拟类的复制行为,可是一般会产生丑陋而且脆弱的语法,好比显式伪多态(OtherObj.methodName.call(this, ...)),这会让代码更加难 懂而且难以维护。
    1. 显式混入实际上没法彻底模拟类的复制行为,由于对象(和函数!别忘了函数也是对象)只能复制引用,没法复制被引用的对象或者函数自己。忽视这一点会致使许多问题。
    1. 在 JavaScript 中模拟类是得不偿失的,虽然能解决当前的问题,可是可能会埋下更多的隐患。(但实际,咱们用得却不少)

第五章 原型

注意:本章的前提是你已经比较熟悉原型及原型链.不太熟或者不知道的能够,经过这篇文章熟悉下.

5.1 [[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.

补充:

  • 使用 for..in 遍历对象时 任何能够经过原型链访问到 (而且是 enumerable:true)的属性都会被枚举。(其实这个在第三章里我说过)
  • 使用 in 操做符 一样会查找对象的整条原型链(不管属性是否可枚举)

5.1.1 Object.prototype

全部普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype。(Object.prototype的[[Prototype]] 最终会指向null.null就是最后的终点). 这个 Object.prototype 对象,包含了 JavaScript 中许多通用的功能,例如:toString() , valueOf(), hasOwnProperty(..)和 isPrototypeOf(..)。

5.1.2 属性设置和屏蔽

说明: 看完本节时,切记不要对myObject.foo = "bar"这种简单的对象属性赋值产生顾虑和疑惑.这种赋值绝对不会对原型链产生任何影响!基本也不会出现赋值不成功的状况.若是有人敢在团队项目里修改对象的属性描述符,早就被拖出去打死了!!! 这部分能够看作补充知识,知道有这些奇葩设定就行.其实这节更应该关注的是myObject.foo的返回值.
注意: 书中提到的动词屏蔽其实指的就是在对象上建立同名属性(原型链上已有该属性).注意不要被绕晕了.还有++就至关于myObject.a=myObject.a+1,注意分解就行,不存在什么特别须要小心的.

5.2 “类”

  • JavaScript里只有对象,没有类!
  • JavaScript不须要经过类来抽象对象.而是本身直接建立对象,并定义对象的行为.

5.2.1 “类”函数

吐槽:模仿类居然被说成奇怪的无耻滥用!?不这样作,js那些高级用法怎么实现?怎么会有如今前端的百花齐放(轮子满地跑)?这也是冒得办法的办法啊!毕竟当时js只是小众,不期望它有多大能耐.毕竟只是一我的用7天"借鉴"出来的东西.

"类"函数: JavaScript用来模仿类的函数就被称为类函数,其实就是咱们常说的构造函数.

"类"函数模拟类的关键: 全部的函数默认都会拥有一个名为 prototype 的公有而且不可枚举(参见第 3 章)的属性,它会指向另外一个对象.当咱们经过new 函数(构造函数)来获得实例对象时,此时new会给实例对象一个内部的 [[Prototype]]属性,实例对象内部的[[Prototype]]属性与构造函数的prototype属性都指向同一个对象.那JS的这个特性怎么模拟类呢?首先类的本质就是复制!.明白这点后,咱们就须要实现伪复制.咱们能够将类里的属性,放在函数的prototype属性里.这样该函数的实例对象就能够经过[Prototype]访问这些属性.咱们也常常把这种行为称为原型继承(做者后面会疯狂吐槽这个称呼,我后面再解释为何吐槽).这样就实现了伪"复制". 能够达到和类类似的效果.

注意: 虽说全部的函数默认都会拥有一个名为 prototype属性.但也有特殊的时候.就不是默认的状况.就是经过bind()硬绑定时.所返回的绑定函数,它是没有prototype属性的!

图解真正的类与JS的模拟类:

关于原型继承这个名字的疯狂吐槽: 做者的吐槽主要集中在"继承"两个字,缘由是在面向类的语言中,"继承"意味着复制,但在JavaScript里原型继承却根本不是这个意思,它并无复制,而是用原型链来实现.因此疯狂吐槽其误导.

什么是差别继承? 我根本没听过这个术语,初次看做者所谓的解释,这是啥?他想说啥?后来读了好多遍,终于大概理解了.若是你也看不懂做者想在表达什么,就pass这部分.不必理解.反而会把你看得更迷惑. 好了,我来解释下什么叫差别继承.差别继承就是原型继承的一个不经常使用的别名.咱们知道对象能够经过原型链继承一部分属性,但咱们仍能够给对象设置其余有差别不一样的属性.这也就能够称为差别继承.

5.2.2 “构造函数”

构造函数之因此是构造函数,是由于它被new调用,若是没被new调用,它就是一个普通函数.实际上,new会劫持全部普通函数并用构造对象的形式来调用它,而且不管如何都会构造返回一个对象.

5.2.3 技术

关于两种“面向类”的技巧,我这就不说明了,理解了这部分第一第二章关于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..遍历到.
复制代码

5.3 (原型)继承

原型对象到原型对象的继承: 例如: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"
复制代码

错误用法:

  • 1, Bar.prototype = Foo.prototype; 此时并不会建立一个关联到 Bar.prototype 的新对象,它只是让 Bar.prototype 直接引用 Foo.prototype 对象。 所以当你执行相似 Bar.prototype. myLabel = ... 的赋值语句时会直接修改 Foo.prototype 对象自己。
  • 2, Bar.prototype = new Foo(); 它使用 了 Foo(..) 的“构造函数调用”,若是函数 Foo 有一些其余操做的话,尤为是与this有关的的话,就会影响到 Bar() 的“后代”,后果不堪设想。

结论: 要建立一个合适的关联对象,咱们需使用 Object.create(..) 而不是使用具备反作用的 Foo(..)。这样作惟一的缺点就是须要建立一个新对象而后把旧对象抛弃掉(主要是须要手动设置constructor),不能直接修改已有的默认对象。

检查"类"关系

  • instanceof 操做符: 验证左边的普通对象的整条[[prototype]]链是否有指向右边函数的prototype,例如:a instanceof Foo
  • isPrototypeOf(..) 方法: 验证在对象 a 的整条 [[Prototype]] 链中是否出现过 原型对象b.例如: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(见第三章).

5.4 对象关联

[[Prototype]] 机制就是存在于对象中的一个内部连接,它会引用其余对象。

这个连接的做用是:若是在对象上没有找到须要的属性或者方法引用,引擎就会继续在 [[Prototype]] 关联的对象上进行查找。同理,若是在后者中也没有找到须要的 引用就会继续查找它的 [[Prototype]],以此类推。这一系列对象的连接被称为原型链

5.4.1 建立关联

问:"咱们已经明白了为何 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以前的语法了.因此这部分你们了解下便可.

5.4.2 关联关系是备用

[[Prototype]] 的本质做用: 书中提到了一个观点"处理“缺失”属性或者方法时的一种备用选项。"(即备用设计模式).但随后进行了否认"可是这在 JavaScript 中并非很常见。因此若是你使用的是这种模式,那或许应当退后一步并从新思考一下这种模式是否合适。" 做者给出的观点是:"进行委托设计模式,即例子中的内部委托(就是在对象里套了个壳再引用了一遍,为的是将委托进行隐藏).这样能够使咱们的API设计得更加清晰."文中的清晰是指,当咱们须要引用原型对象的属性方法时,咱们在对象内部设置对应专门的属性(例子中的doCool),进行内部委托(其实就是套个壳进行隐藏).这样咱们对象的属性就是"完整"的.

在实际工做中,咱们经常就是把原型对象做为存放对象的公共属性方法的地方.对于通常比较重要的操做才会在对象里进行内部委托(隐藏委托)!

5.5 小结

总结得很好很全面,这里我仍是直接摘抄了,不是偷懒哦!

  • 若是要访问对象中并不存在的一个属性,[[Get]] 操做(参见第 3 章)就会查找对象内部[[Prototype]] 关联的对象。 这个关联关系实际上定义了一条“原型链”(有点像嵌套的做用域链),在查找属性时会对它进行遍历。
  • 全部普通对象都有内置的 Object.prototype, 指向原型链的顶端(好比说全局做用域),如 果在原型链中找不到指定的属性就会中止。toString()、valueOf() 和其余一些通用的功能 都存在于 Object.prototype 对象上,所以语言中全部的对象均可以使用它们。
  • 关联两个对象最经常使用的方法是使用 new 关键词进行函数调用,在调用的 4 个步骤(第 2 章)中会建立一个关联其余对象的新对象。
  • 使用 new 调用函数时会把新对象的 .prototype 属性关联到“其余对象”(就是构造函数prototype所指的对象)。带 new 的函数调用 一般被称为“构造函数调用”,尽管它们实际上和传统面向类语言中的类构造函数不同。
  • JavaScript 中的机制有一个核心区别,那就是不会进行复制,对象之间是经过内部的 [[Prototype]] 链关联的。
  • “委托”是一个更合适的术语,由于对象之间的关系不是复制而是委托。(意思就是原先继承应该改成原先委托?)

第六章 行为委托

第 5 章的结论:[[Prototype]] 机制就是指对象中的一个内部连接引用另外一个对象。换句话说,JavaScript 中这个机制的本质就是对象之间的关联关系。在第六章又被称为委托. PS:前面在讲原型的时候我就习惯用父对象指代原型对象(相似"父类"),用子对象指代其实例对象(相似"子类").本章也将采用这种称呼,故下面再也不说明.(其实我以为用父对象和子对象称呼更形象)

6.1 面向委托的设计

一句话:[[Prototype]]机制是面向委托的设计,是不一样于面向类的设计. 下面将分别介绍类理论和委托理论.

6.1.1 类理论

类理论设计方法: 首先定义一个通用父(基)类,在 父类类中定义全部任务都有(通用)的行为。接着定义子类 ,它们都继承自 父类而且会添加一些特殊的行为来处理对应的任务,而且在继承时子类能够使用方法重写(和多态)父类的行为.

类理论中许多行为能够先“抽象”到父类而后再用子类进行特殊化(重写)。 ps:这部分了解便可,着重理解下面JavaScript用到的委托.

6.1.2 委托理论

类理论设计方法: 首先你会定义一个"父"对象(至关于上节中的父类),它会包含全部任务均可以使用(委托)的具体行为。接着,对于每一个任务你均可以定义一个对象("子"对象)来存储对应的数据和行为。你能够把特定的任务对象都关联到父对象上,让它们在须要的时候能够进行委托。 (其实咱们通常都是用父对象来定义通用的方法,子对象进行委托.而后子对象自身个性的属性方法就写在子对象自己,并避免与父对象的属性名冲突)

ps: 这节书中这段话可是咱们并不须要把这些行为放在一块儿,**经过类的复制**,咱们能够把它们分别放在各自独立 的对象中,须要时能够容许 XYZ 对象委托给 Task。有个错误."经过类的复制"应该改成"经过"[[Prototype]]机制".这里应该是做者的手误. 在 JavaScript 中,[[Prototype]] 机制会把对象关联到其余对象。不管你多么努力地说服自 己,JavaScript 中就是没有相似“类”的抽象机制。(其实主要缘由仍是是JavaScript没有完整的复制机制)

委托理论的使用建议:
PS:书中这里写了3条,其实只有2条,第三条不过是对第一条的说明,这里我进行了合并.

    1. 一般来讲,在 [[Prototype]] 委托中最好把状态保存在委托者(子对象)而不是委托目标(父对象)上。那怎么实现呢,就是经过"this 的隐式绑定".在委托目标(父对象)上的函数里经过this定义保存状态.当委托者(子对象)引用该函数方法时,此时的this就自动绑定到委托者上了.
    1. 在委托中咱们会尽可能避免在 [[Prototype]] 链的不一样级别中使用相同的命名,不然就须要使用笨拙而且脆弱的语法来消除引用歧义(参见第 4 章)。
  • 补充: 3. 在 API 接口的设计中,委托最好在内部实现,不要直接暴露出去。 这么作更多的是出于安全和接口稳定的考虑.建议子对象将全部引用父对象的方法都套个函数隐藏起来,并取一个语义化的属性名.

委托理论的使用注意:

    1. 禁止两个对象互相委托:当你将第二个对象反向委托给前一个对象时,就会报错.
    1. 调试: 这个了解下就行.知道不一样浏览器和工具对委托的解析结果并不必定相同.(吐槽:看了半天到后面出现实际上,在编写本书时,这个行为被认定是 Chrome 的一个 bug, 当你读到此书时,它可能已经被修复了。我只想说WTF! 好吧,我知道chrome之前可能出现过这个"bug"了=.=)

6.1.3 比较思惟模型

这节主要是比较了"经过构造函数(模拟类)实现原型继承"与"经过对象关联(委托形式,Object.create( ... ))实现原型继承"两种方式的区别.

结论: 经过对象关联,委托形式,更加简洁,更加清晰易懂.

PS:这里我本来本身对例子画出原型示意图.可是发现是真的复杂,而且和书中简洁后的示意图是差很少的,因此这里就不展现了,省得让读者看得更头大.这里建议,读者本身在草稿纸上画出原型示意图.

6.2 类与对象

其实这节讲得仍是"经过构造函数(模拟类)实现原型继承"与"经过对象关联(委托形式,Object.create( ... ))实现原型继承"两种方式的区别.不过此次主要是之前端实际使用场景进行讲解.

6.2.1 控件“类”

这里我就不以书中的例子进行讲解了,而是直接站在更高的角度对这种"类"风格的代码进行讲解.
最大特色: 1是经过构造函数进行模拟类,2是经过显式伪多态(硬绑定函数)关联两个函数.
注意:

  • 无论是类仍是对象.这两种形式通常都须要定义两种数据.第一种就是实例对象要用到的"初始保存的数据";第二种就是通用行为的定义,包括对实例对象数据的增删改查.
  • 下面提到的显式伪多态(详见第四章),其实指的就是使用call()方法这种硬绑定.
  • 注意ES6 class模拟类的写法我就没具体列出了.实际上class 仍然是经过 [[Prototype]] 机制实现的,不过是个表面简洁的语法糖.

虽然书中对显式伪多态称为"丑陋的",还用了一个语气动词"呸!".虽然这样很差,但有时用call真的很方便,因此用得也不少.

6.2.2 委托控件对象

最大特色: 经过对象载体来模拟父子,并经过Object,create(...)来对两个对象进行关联.并经过委托的形式进行引用.与上节中提到的类形式还有一个区别:对象foo构建后,须要手动调用setUp方法进行初始化.故对象的构建与初始化是分开的.而构造函数形式则是在new 构造函数时, 同时进行了对象构建与初始化.(关于这点我下面还会再说明的)

关于书中这句使用类构造函数的话,你须要(并非硬性要求,可是强烈建议)在同一个步骤中实现构造和初始化。然而,在许多状况下把这两步分开(就像对象关联代码同样)更灵活。的理解:使用类构造函数形式,当咱们使用new 构造函数时,实际上是在一步实现对象的构建和对象数据的初始化(经过构造函数里的call) ;使用这种委托形式,咱们是分别经过Object.create( ... );构建对象和foo.setUp( ...);来初始化的.即咱们是分两步实现的.这样分开的话实际上是更加灵活,也更符合编程中的关注分离原则.

6.3 更简洁的设计

这节也是同样经过二者的对比来突显委托设计模式的各类优势.这里我就再也不对书中的例子进行解读.若是你真正理解了类和委托的话,实际上是很简单的.若是以为复杂的话,能够在纸上理一下函数和对象之间的关系,下面我就只总结下这里提到委托设计模式的优势,固然核心是更简洁.

简洁体如今:

  • 1, 委托重点在于只须要两个实体(两个对象相互关联),而以前的"类"模式须要三个(父"类",子"类",实例对象)其实能够这么理解:委托模式将"子类"和"实例对象"合为一个对象了。
  • 2, 不须要基类(父类)来"共享"两个实体之间的行为.不须要实例化类,也不须要合成.其实这第二条就是对第一条这种结果的说明.
  • 额外补充强调:在使用构造函数模拟类时,子类一般会对父类的行为进行重写(属性名相同);但委托模式则不会,它会从新取个属性名,再引用父对象上的行为.

6.4 更好的语法

这节主要是介绍ES6提供的2个简洁写法与其中的隐患.

语法:

  • 在 ES6 中咱们能够在任意对象的字面形式中使用简洁方法声明,例如:
var Foo = {
 bar() { /*..*/ },//字面形式声明
};

复制代码
  • 在 ES6 中咱们能够用 Object. setPrototypeOf(..) 来修改对象的 [[Prototype]],具体用法能够查看MDN例如:
// 使用更好的对象字面形式语法和简洁方法 
var AuthController = {
         errors: [],
         checkAuth() {
           // ... 
         },
         server(url,data) {
             // ...
         }
         // ... 
};
// 如今把 AuthController 关联到 LoginController 
Object.setPrototypeOf( AuthController, LoginController );
复制代码

弊端:

  • 对象字面形式语法:实际上就是一个匿名函数表达式.匿名函数会致使3个缺点:1. 调试栈更难追踪;2. 自我引用(递归、事件(解除)绑定,等等)更难; 3. 代码(稍微)更难理解。(其实我以为这3个缺点还好,影响不是那么大).可是这种简洁语法很特殊,会给对应的函数对象设置一个内部的 name 属性,这样理论上能够用在追 踪栈中。因此实际上惟一的弊端就只剩第2条了.终极建议就是:若是你须要自我引用的话,那最好使用传统的具名函数表达式来定义对应的函数,不要使用简洁方法。
  • Object. setPrototypeOf(..) 这个是书中没有提的,我以为有必要进行补充下.首先,Object. setPrototypeOf(..)可能会带来性能问题,若是关心性能,则应该使用Object.create()替代.Object. setPrototypeOf(..)与Object.create()的主要区别: Object. setPrototypeOf(..)会直接修改现有对象的[[prototype]],Object.create()则是返回一个新对象.因此你须要手动设置一下丢失的的constructor属性(若是你须要的话).而使用setPrototypeOf(..)则不须要.

6.5 内省

吐槽: 纵观整本书,做者关于JavaScript中模拟类和继承"的批评,说它们具备很大误导性!更是嗤之以鼻!就差爆粗口了,JavaScript就像一个异教徒,应该绑在十字架上被烧死!可是他这样的观点,都是站在其余语言的角度来看待时,产生的.我想更多的读者多是只接触过JavaScript.那么他实际上是没有这些疑惑的!!!你反而给他们讲这一大堆其余语言的"正确"含义,有时候会时得其反!让读者更加困惑,若是是理解不透彻的,反而会怀疑本身原本写的是对的代码!因此读者应该作一个能够理解做者意图,而且拥有自我看法和观点立场!

什么是内省(自省)? 首先,本节须要弄懂一个问题,什么是内省,或者是自省。书中的解释是自省就是检查实例的类型。类实例的自省主要目的是经过建立方式来判断对象的结构和功能。我这里再更通俗地解释下:当咱们构建获得一个实例对象时,有时候咱们是不太清除它的属性和方法的.尤为是第三方库.有时候贸然使用会致使不少错误(例如调用的方法不存在,或者报错等).这个时候咱们就须要经过自省.其实就是经过一系列操做,来确认实例是否是咱们想要的那个,实例的方法是否是咱们想要的(存在且可用).

内省的方法:

  • 1.经过 instanceof 语法:
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进行判断.而且也不能直接判断两个对象是否关联.

  • 2.经过 "鸭子类型": 为何会叫这个名字?看了做者的解释,仍是不太能接受.不太理解外国人的脑回路.你在国内和别人说"鸭子类型",估计也是一脸懵逼.其实很简单,所谓的"鸭子类型"其实也是咱们实际工做中经常使用的:
//若是a1的something存在的话,则咱们能够进行调用
if ( a1.something) { 
  a1.something();
}
复制代码

其实这种方法是很是经常使用的,排除了在不知道存在性状况下,贸然调用的风险.
缺陷: 关于书中提到的缺点,四个字归纳就是"以偏概全" .书中关于Promise的例子,就是以偏概全的例子.因此咱们在使用时,在if判断a1.something存在时,才会在后面使用something方法.不要直接使用anotherthing,这种没确认过的方法.

  • 3.若是使用对象关联时: 则能够比较简单的使用Object.getPrototypeOf(..)进行判断.例如Object.getPrototypeOf(a)===A其中a,A都是对象.若是为true,则说明a的原型链上含有对象A.

6.6 小结

  • 除了类和继承设计模式,行为委托实际上是更强大,更值得推广的模式(本观点仅表明做者的观点!)
  • 行为委托认为对象之间是兄弟关系,互相委托,而不是父类和子类的关系。JavaScript 的 [[Prototype]] 机制本质上就是行为委托机制。(我以为仍是父子对象关系.个人解说里也都是父子相称)
  • 当你只用对象来设计代码时,不只可让语法更加简洁,并且可让代码结构更加清晰。
  • 对象关联(对象以前互相关联)是一种编码风格,它倡导的是直接建立和关联对象,不把它们抽象成类。对象关联能够用基于 [[Prototype]] 的行为委托很是天然地实现

后续更新

后面关于<你不知道的JavaScript>中和下.还在写做当中,手头上还有一篇webpack彻底指北的文章,目前写了一半2w字,也都是面向新手,真正的全面地由浅入深.最近,空降一个新项目组,开发到测试只有一个月,还要带新人,更新会很慢.不过我会争取年前所有放出.若是你们喜欢的话,能够关注我一下,文章首发仍是在掘金的.

最后求一个内推,目前笔者在一家软件国企(半养老型).年末或者明年初就会考虑离职.但愿进入一家比较好的互联网企业.若是有小伙伴有好的机会能够发我邮箱:bupabuku@foxmail.com.谢谢!
目前暂时优先考虑武汉(房子,盆友,东西都在这边,去外地太不方便了-.-)

百度网盘下载

为了方便你们下载到本地查看,目前已经将MD文件上传到百度网盘上了. 连接: pan.baidu.com/s/1ylKgPCw3… 提取码: jags (相信进去后,大家还会回来点赞的! =.=)