全方位彻底读懂<你不知道的JavaScript(上)>--一篇六万多字的读书笔记

35,773 阅读1小时+

前言

Q&A

  • 1.问:为什么要写这么长,有必要吗?是不是脑子秀逗了?
    答:我想这是大部分人看到这个标题都会问的问题.因为作为一个男人,我喜欢长一点,也不喜欢分割成几个部分.一家人就要在一起,整整齐齐.好吧,正经点,其实整篇前言可以说都是在回答这个问题.你可以选择先看完前言,再决定要不要和书本搭配起来阅读. 这里先简单捋一下:1,内容多:首先这篇读书笔记本来内容就很多,是对书本的全方位详解.2,针对新人:针对那种红宝书草草读过一遍,对js只浮于接口调用的新手.3,留给读者自己提炼:读这种社科类书籍一般是先读厚,再读薄.这篇笔记就属于最开始'读厚'的阶段.在读者彻底读懂后,再自己进一步提炼.关于怎么读书,我后面会详细介绍.

  • 2.问:这么长,那到底包含了些什么内容?
    答:笔记的目录结构和书本的完全一致.对每一节的内容进行更通俗的解读(针对新人),对示例进行更深的说明,有的会辅以流程图,并提供对应的mdn连接;对内容进行归纳,小节脉络更清晰;添加了大量实际工作时的注意事项,增加了更加清晰和易懂的示例及注释,并在原文基础上进行了拓展和总结;对书中的错误和说了后面会进行介绍,而没有介绍的填坑,翻译或者容易引起误会的称呼的说明;添加了个人读书时的感受和吐槽.

  • 3.问:书已经够多了,还要看你这么长的笔记?
    答:首先你要知道读这种技术类书籍,不是读小说!读完并不意味着你读懂了.而是需要将书中的知识转换成你自己的.这篇笔记就是,帮助新手更方便地理解知识点,更流畅地进行阅读.也可以在读完一节后,通过对比,发现自己有什么知识点是不懂或者遗漏,理解有误的. 并且一些注意事项,容易被误导的,关于书中观点的吐槽等等,其实想说的都已经写在笔记里了.

  • 4.问:这本书到底怎么样,有没有其他人说的那么好?
    答:这是一个先扬后抑的回答.首先毫无疑问这是一本非常不错的书!它系统地全面地对JavaScript进行解读,优点缺点全都有.当你彻底读懂这本书后,你对JavaScript的几乎所有疑问都会得到解答(我对作用域是不是"对象"的疑问?也得到了解答).但它也是有一定门槛的,如果你对JS不熟,常用接口都不熟,很多名词的表层意思都不太理解.这本书并不适合你,你花在问谷歌娘的时间可能比你读书的都长,读起来也是一知半解;不同于其他书,这本书很多时候没有给出明确的概念定义,需要你自己反复阅读理解他的话.每一小节的脉络结构也不是那么清晰,有时候需要自己去梳理;不知道是不是翻译的锅,很多东西解释得有点迷,本来很简单,但却说一堆并不常用的术语(可能国内不是这么叫的),看得你一脸懵逼!有时候同一个概念,前后会出现三四个不同的名词进行指代,没有任何说明;整本书,具有很强的作者主观情感在里面.前半段,把JS捧得很高,说它引擎的各种优化好!但到后半段关于JavaScript中模拟类和继承"的批评,说它们具有很大误导性!更是嗤之以鼻!就差爆粗口了,好像JavaScript就是一个异教徒,应该绑在十字架上被烧死!但是他这样的观点,都是站在其他类语言的角度来看待,产生的.我想更多的读者可能是只接触过JavaScript这一种语言,对他们来说,其实是根本没有这些"疑惑"的!

读书建议:

  • 1.不要抱任何功利和浮躁的心来读书!
    这种以理论,概念为主的书,其实大家都是不那么愿意读的.一是读起来很费劲,抽象.二是实际工作,几乎不会用到,在现在浮躁的前端圈这是吃力不讨好.那这本书最大的用处是什么?没错,就是被很多人用来应付面试!? 这本身没什么问题,你读懂系列三本书,所有涉及JS的面试都能轻松应对.但是当抱着功利心时,你更多的则是敷衍.对书中的概念进行机械的复制,再粘贴上自己肤浅的理解.OK,应付那些也是跟风的面试官足够了.一般你回答了,他们也不会继续往下问,问深了自己也不清楚,也不好否定你.如果你够自信,'瞎扯'也可以唬住.如果你答不上,脸皮厚的会让你回去自己查.真正知道的面试官,其实都是会给你解释的,他们也不会忙到差这点时间.其实他们心里也是很乐意展示自己学识丰富的一面.
    这种功利读书方式,即使你读完了(更多人是半途而废),对你的技术也不会有任何帮助.因为读完,你其实是一知半解的.这样反而更糟,甚至可能会对你之前JavaScript正确的理解产生混淆.

  • 2.认认真真读完一本书好过收藏一百篇相关文章(其实你压根连一半都不会看)!

我一直认为想系统弄懂一门知识,书本才是最好的选择,它绝对比你东拼西凑找来的一堆文章要好得多!现在前端圈随便看看,一大堆全是原型链,闭包,this...这些内容.里面的内容大同小异,很多理解也是比较浅显,考虑的也比较片面.但浮躁的人就是喜欢这种文章,觉得自己收藏了,看了就彻底理解了(!?).其实这些文章里有很多都是借鉴了本书.

首先,你必须知道知识都是有体系的,不是完全独立的.例如想要彻底理解,原型链,闭包,this.就必须先弄清作用域和函数.知识都是环环相扣,相互关联的.如果你想彻底弄懂,还是选择读书吧,由浅入深,全面理清所有知识点的关联.记住 "一知半解"永远比"无知"更糟!(当然不懂装懂,还振振有词的人另当别论).

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

第一部分 作用域和闭包

第一章 作用域是什么

1.1 编译原理

传统编译的三个步骤

  • 1,分词/词法分析(Tokenizing/Lexing) : 这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如,考虑程序var a = 2;。这段程序通常会被分解成 为下面这些词法单元:var、a、=、2、;。空格是否会被当作词法单元,取决于空格在 这门语言中是否具有意义。
  • 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 对话(代码编译执行过程)

JavaScript对var a =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的解释)

configurableenumerablevaluewritablegetset
数据描述符YesYesYesYesNoNo
存取描述符YesYesNoNoYesYes

如果一个描述符不具有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]] 的行为委托非常自然地实现

求内推

如果有好的前端岗位,欢迎内推我,谢谢。

本科,三年运维,五年前端。 base 武汉 邮箱 bupabuku@foxmail.com

百度网盘下载

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