JS高程(二)

面向对象 的 程序设计

    ECMAScript 中 有 两种 属性: 数据属性 和 访问器属性

     数据属性

        数据属性 包含 一个数据值 的 位置, 在这个 位置 可以 读取 和 写入值。数据属性 有 4 个 描述 其 行为的 特性。(为了表示这些 特性 是 内部值,该规范 把 它们放在了两对 方括号中,比如 [ [ Enumerable ] ] )

        1 [ [ Configurable ] ]: 表示 能否 通过 delete 删除 属性 从而 重新定义属性, 能否 修改属性的 特性,或者 能否 把属性 修改为 访问器 属性。默认值 为 true.

         2 [ [ Enumerable ] ] : 表示 能否 通过 for- in 循环返回属性。 默认值 是 true

        3 [ [ Writable ] ] : 表示 能否 修改该属性 的 值。默认 为 true

        4 [ [ Value ] ]: 包含 这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把 新值保存在这个位置。默认值为 undefined。

        要修改 属性默认的 特性,必须使用 Object.defineProperty(),接收三个参数:属性所在的对象、属性的名字和一个 描述符对象。其中,描述符(descriptor)对象的属性必须是:Configurable、Enumerable 、Writable 、Value 

JS高程(二)_第1张图片
把Configurable 设置为 false , 表示 不能 删除属性  

       一旦 把 属性 定义为不可配置的(Configurable 设置为 false),就不能 再 把它 变回 可配置的了。

访问器属性

            访问器属性不包含 数据值,它们包含 一对 getter 和 setter 函数( 不过,这两个函数都不是必须的)。在 读取 访问器 属性时,会 调用 getter 函数 ,这个函数 负责 返回 有效的值;在写入 访问器属性时,会调用 setter 函数 并传入 新值,这个 函数 负责 决定 如何处理数据。

          访问器属性 有如下 4 个特征:

                1 [ [ Configurable ] ]: 表示 能否通过 delete 删除 属性从而 重新定义属性,能否修改 属性的特征,或者 能否把属性修改改为 数据属性。默认值 为 true

                2 [ [ Enumerable  ] ]: 表示 能否 通过 for-in  循环返回 属性。默认 为true

                3 [ [ Get ] ]: 在 读取属性 时调用的函数。 默认值 为 undefined

                4 [ [ Set ] ]: 在 写入属性 时调用的函数。 默认值 为 undefined

JS高程(二)_第2张图片
不一定 非要同时指定get 和 set  

            定义多个属性

                Object.defineProperties()

JS高程(二)_第3张图片
defineProperties

         读取属性的特性

                Object.getOwnPropertyDescriptor() 可以 取得 给定属性的 描述符, 这个方法接受两个 参数: 属性所在的对象 和 要读取其描述符的属性名称。 返回值 是一个对象,如果是 访问器属性,这个 对象的属性 有,configurable、enumerable、get  和 set;如果 是 数据属性,这个对象的属性有 configurable、enumerable、 writable、value

JS高程(二)_第4张图片
getOwnPropertyDescriptor  

     创建对象

        虽然 Object 构造函数 或 对象字面量 都可以 用来 创建 单个对象,但这些方式有个明显的缺点,使用 同一个 接口创建 大量重复的代码。为解决这个问题,人们开始 使用工厂模式

 工厂模式

JS高程(二)_第5张图片
工厂模式

          工厂模式没有解决对象识别问题(即 怎么知道一个对象的类型)

构造函数模式

JS高程(二)_第6张图片

        与 createPerson 不同的是: 1 没有显示的 创建对象; 2 直接将属性和方法赋给了this 对象;3 没有return。

        构造函数 函数名开头 大写。 构造函数 本身 也是 函数 ,只不过 可以用来 创建对象而已。

        要创建 Person 的新实例,必须要使用 new 操作符。以这种 方式调用构造函数会经历以下 4 个 步骤:

        1 创建一个新对象

        2 将 构造函数 的作用域赋给新对象(因此 this 就指向了这个新对象)

        3 执行构造函数中的代码(为这个新对象添加属性)

        4 返回新对象

        上面 person1 和 person2 分别 指向 Person 的两个 不同的 实例。这两个对象 都有一个 constructor 属性,该属性 指向 Person

           person1.constructor  === Person;

           person2.constructor  === Person;

           person1 instanceof Object ; // true

           person1 instanceof Person ; // true

         创建 自定义的 构造函数 意味着 将来 可以将它 的实例 标识为一种特定的类型(比如 标识 为 Person这个 类);而这正是 构造函数模式 胜过 工厂模式的地方。

  1  将构造函数 当作函数

        构造函数 与 其他函数 的 唯一区别, 就是调用 它们的 方式不同。不过 构造函数 毕竟也是函数,不存在 定义构造函数 的特殊语法。任何函数,只要通过 new 操作符来调用,那 它就可以作为构造函数;同时,如果 不通过 new 操作符来 调用,那 它 跟普通函数 也不会有什么两样。

JS高程(二)_第7张图片

2 构造函数的问题

         使用构造函数的主要 问题 就是 每个方法 都要在每个实例上 重新创建一遍。

        前面的例子中 person1 和 person2 都有 一个 sayName() 方法,但是 它们 不是同一个 Function的 实例。就是说 ,它们 是指向 两个不同 的 函数对象,但是这两个函数对象 完成的是相同的任务

        person1. sayName == person2 .sayName; //  false  它们指针 指向不同的对象

JS高程(二)_第8张图片
可以通过 函数 定义 转移 到构造函数外 部来解决这个问题  

         同时,这样处理 带来的 问题就是 sayName() 在 全局 中, 造成了 变量名 的 污染。所以 我们可以 通过以下方法 来解决。


        原型模式

            我们 创建 的 每一个函数 都有一个 prototype(原型)属性,这个属性 是一个 指针,指向 一个对象, 而这个对象的用途 是 包含可以 由特定类型 的 所有 实例共享的 属性 和 方法.

JS高程(二)_第9张图片
prototype

        使用原型对象的好处就是可以让所有对象实例 共享 它所包含的属性和方法,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中.

   理解 原型对象

        无论 什么 时候 , 只要创建一个 新函数, 就会 根据一组特定的 规则,为该函数创建一个 prototype 属性. 这个 属性 指向 函数 的 原型对象. 在 默认 情况 下, 所有 原型对象 都会 自动 获得 一个 constructor 属性.  这个 属性 包含 一个 指向 prototype 属性所在函数 的 指针.

JS高程(二)_第10张图片

        创建了 自定义的 构造函数 之后, 其 原型对象 默认 只会 取得 constructor 属性; 至于 其他方法 都是 从 Object 继承 而来的.  当 调用 构造函数 创建 一个 新实例 后, 该 实例 的 内部将 包含 一个 指针(内部属性), 指向 构造函数 的原型对象.  虽然 没有标准的访问 [ [ prototype ] ] 的 ,但是 Firefox, safari  和 Chrome 在 每个 对象上 都支持 一个 属性 __proto__ , 而在其他 实现 中,这个 属性 对 脚本 是 完全 不可见的.  不过 要明确的 真正重要 的一点 就是这个 连接存在于 实例 和 构造函数的原型对象 之间, 而 不存在于 实例 与 构造函数 之间.

        虽然 在所有 实现中 都 无法 访问到 [ [ Prototype ] ] , 但是 可以通过 isPrototypeOf() 方法 来确定 实例  和 原型 之间 的联系 ,  返回 布尔值.

        Person.prototype.isPrototypeOf ( person6 ) ; // true

 Object.getPrototypeOf ()方法 返回 [ [ Prototype ] ] 的 值.( 就是 实例对象 的 原型 )

        Object.getProtorypeOf (person6) === Person.prototype;

         当为 实例对象 添加一个 属性时, 这个 属性 会屏蔽 原型对象中 保存的 同名属性( 也就是 在 原型链 中 寻找 属性 ) .使用 delete 操作符 删除 实例上面的 属性, 可以让我们 访问到原型 上的 同名属性.

         hasOwnProperty() 方法 可以检测 一个属性 是否 存在于 实例中, 还是 原型中. 只有 给定属性存在于 对象实例 中 才返回 true .

        person6.hasOwnProperty ( ' name ' ); // false ,因为  name 在 原型上.


   原型 与 in 操作符

        有两种 方式 使用 in 操作符: 单独使用 和 在 for-in 循环 中使用.单独使用过程中, in 操作符 会 在通过 对象 能够访问给 定属性时 返回 true, 无论该 属性 存在 于 实例 中 还是原型中. 这点跟 hasOwnProperty  不同.

            ' name ' in person6 ; // true , 不论该属性 是在 实例 还是 原型中

JS高程(二)_第11张图片
函数返回 值 为 true ,确定属性 在 原型上  

    在使用 for-in 循环 时, 返回 的 是 所有能够 通过对象 访问的可枚举的( enumerated ) 属性, 其中 既 包括 存在于 实例中的属性, 也包括 存在原型 中的 属性.

    要想 取得 对象上 所有 可枚举 的 实例属性, 可以 使用 Object.keys( obj )  , 它 返回 一个包含所有 可枚举属性 的 字符串数组.

    如果 要想 取得所有 实例属性, 无论 是否 可枚举, 可以使用 Object.getOwnPropertyNames( obj )


更简单的 原型语法

        前面例子 每 添加 一个 属性 和 方法 就要 敲一遍 Person.prototype.为了减少 不必要的输入, 更常见的做法 是 用字面量 来重写 整个 原型对象.

JS高程(二)_第12张图片

原型的 动态性

    如果 在 创建实例之后 修改了原型 ,  但是 该 实例 的 .__proto__ 指向 还是 修改 之前的 原型对象, 所以 没有 sayName() 方法 ; 而 在 修改原型 之后  创建实例, 那么 实例 指向的 是 新的原型对象.  请记住: 实例中的 指针 仅仅 指向原型, 而不指向 构造函数 !!!!!

JS高程(二)_第13张图片
动态 修改 原型

原生对象的原型

    原生模式 的 重要性 不仅体现在 创建 自定义类型放面, 就 连 所有 原生的引用类型 ,都是采用 这种模式.所有 原生类型 ( Object, Array, String ) 都 在 其构造 函数 的 原型 上 定义了 方法. 比如 , 能在 Array.prototype 中 找到 sort() 方法, 而在 String.prototype 中 可以找到 substring() 方法.

    尽管 可以 在原生类型 的原型上添加 方法 ,  但是 并 不推荐这么做.

 原型对象的问题

        1 它忽略了 为 构造函数 传递 初始化 参数 这一 环节, 结果 所有 实例 在默认 情况下 都取得相同的属性值 ( 在原型上定义 了 固定的 属性值 )

        2  最大的 问题 是 由 其 共享的 本性所导致的 , 原型 中 所有 属性 是 被 很多实例共享的,这种共享 对于 函数非常适合.  但是 , 对于 包含 引用类型值 的属性 来说, 就出现了问题, 比如 下面 示例 中 的 friends:

JS高程(二)_第14张图片
原型对象的问题

        两个 实例 共用了 一个 数组,  任何 一个 实例 修改 数组,  另外 一个 实例的 值 也 相应的改变了. 这个 问题 正是 我们 很少看见 有人 单独使用 原型模式的 原因所在.

   组合使用构造函数模式和原型模式

        创建 自定义类型 最常见方式, 就是 组合使用 构造函数模式 和 原型模式.

        构造函数模式 用于 定义 实例属性, 而 原型模式 用来 定义方法 和 共享的属性. 每个实例都会由自己的一份实例属性的副本, 但又同时共享着 对 方法的引用, 最大限度地 节省内存. 另外这种模式还支持向 构造函数传参数, 可谓集 两种模式之长.

JS高程(二)_第15张图片
 组合使用 构造函数模式 和 原型模式  

动态原型模式

    动态原型模式 是为了解决 独立的构造函数 和 原型 , 它 将 所有 信息 都封装 到了构造函数中 , 而 通过在 构造函数 中 初始化原型( 仅 在必要的 情况下), 又 保持了 同时 使用 构造函数 和 原型 的优点.  简单的来说 , 可以通过 检查 某个 应该存在的方法 是否有效, 来决定 是否 需要初始化原型.

JS高程(二)_第16张图片
动态原型模式 sayName 不存在的情况下 , 修改原型  

    if 这段代码 只有在 初次 调用 构造函数 的时候才会执行, 此后, 原型 已经 完成 初始化.  if 语句 检查 的 可以 是 初始化之后 应该 存在的 任何属性 或者 方法 , 不必 用 一大堆 的 if 语句 检查 每个属性 和 每个方法 , 只要检查 其中 一个 即可.

寄生构造函数模式

    通常, 在 前集中模式 都不 适用  的 情况下,  可以 使用 寄生构造函数模式. 这种模式的 基本思想 是 创建 一个 函数, 该函数 的 作用 仅仅 是 封装 创建对象 的代码, 然后 再返回新创建的对象; 但 从表面上 看 这个函数 又 很像 是 典型的 构造函数.

    如果在构造函数中,有 return 语句, 如果 return 的 是 值类型的 ,那么对构造函数没有影响,还是 返回 原来的 实例对象 ;如果 return 引用类型(数组,函数,对象),那么实例化对象就会返回该 引用类型。

JS高程(二)_第17张图片
 寄生构造函数模式

除了 使用 new 操作符 并把使用 的 包装函数 叫做 构造函数之外 , 这个模式 跟 工厂模式 其实一模一样. 构造函数 不返回值的 情况下, 默认会 返回 新对象实例.

这个模式的 用途 是实现在不改变原有构造函数的前提 下为 构造函数 添加特殊 的方法。

JS高程(二)_第18张图片

    关于 寄生 构造函数模式,有一点 需要 说明的 是:返回的对象  构造函数 或者 与 构造函数的原型 之间 没有关系, 也就是 说 构造函数返回 的 对象 与 构造函数 外部 创建的 对象 没有什么不同。因此 不能 依赖 instanceof 操作符来 确定 对象 类型。由于 存在上述 问题, 我们建议 可以 使用 其他模式的情况 下,不要使用这种模式。

稳妥构造函数模式

    所谓 稳妥对象,指的 是 没有公共 属性,而且 方法 也不 引用 this 的 对象。稳妥对象 是 最适合 在一些安全的 环境 中( 这些环境 中 会 禁止使用 this  和 new ), 或者 在 防止 数据被 其他应用程序改动时使用。稳妥构造函数 遵循 与 寄生构造函数 类似 的模式,但是两点不同:一是 新创建对象的 实例方法不 引用 this ;二是 不使用 new 操作符调出 构造函数。

JS高程(二)_第19张图片

    在这种模式 创建的 对象中,除了 使用 sayName() 方法 之外,没有 其他 办法 访问 name 的值。friend 中 保存一个 稳妥对象,除了sayName() 没有别的方式 可以访问 其他数据成员,即使有其他代码 会给这个对象 添加 方法或数据成员,但是也不可能有别的办法访问 传入到构造函数中 的 原始数据。 稳妥 构造函数模式 提供的这种安全性,使它非常适合在 某些 安全执行环境 提供的 环境下使用。

        与 寄生构造函数 模式类似,使用 稳妥构造函数 模式创建的 对象与 构造函数之间也没什么关系,因此 instanceof 操作符 对这种对象也没有意义。

        friend instanceof Person; // false


继承

原型链

    构造函数、原型 和 实例 的关系:每一个 构造函数 都有 一个原型,原型对象 都包含一个 指向 构造函数的指针(constructor),而 实例都包含 一个指向原型对象 内部的指针(__proto__)。

JS高程(二)_第20张图片
原型链
JS高程(二)_第21张图片
实例

    所有 引用类型 默认继承了 Object , 而 这个继承 也是 通过 原型链 来 实现的。

JS高程(二)_第22张图片

    可以通过两种方式来 确定 原型 实例 之间的 关系。第一种方式 是 instanceof 操作符,只要 用这个 操作符 来测试 实例 原型链 中出现过的构造函数,结果 就会返回 true。

    instance instanceof Object; // true

    instance instanceof SubType;  // true

    instance instanceof SuperType;  // true

    另一种方式是使用 isPrototypeOf() 方法。 同样,只要是 原型链中 出现过的原型,都可以说是 改原型链派生的 实例 的 原型。

    Object.prototype.isPrototypeOf(instance); // true

    SubType.prototype.isPrototypeOf(instance);  // true

    SuperType.prototype.isPrototypeOf(instance);  // true

  谨慎地定义方法

    子类型 有时候 需要重写 父类型 中的 某个方法,或者 需要添加 父类型中 不存在的 某个方法,但是 不管怎么样,给原型 添加方法 的 代码 一定要放在 替换原型 的 语句之后。

JS高程(二)_第23张图片

    即使 通过 原型链 实现 继承时, 不能使用 对象字面量 创建原型方法,因为这样 做 就会重写 原型链。

JS高程(二)_第24张图片
字面量定义 原型 会重写原型链  

    原型链的问题

原型链的 最主要的 问题 来自 包含 引用类型的 原型。前面 friend 中的 例子中介绍过,包含 引用类型值 的 原型属性 会被 所有 实例共享; 而这样正是 为什么 要在构造函数 而不是 在原型中 定义属性的 原因

        在通过原型来实现继承 时,原型实际上会变成另一个类型的实例,于是,原先的 实例属性也就 顺理成章地 变成了 现在的原型属性了。

JS高程(二)_第25张图片
   原型链的问题

    原型链的 第二个问题: 在创建 子类型的 实例时,不能向 父类的构造函数中 传递参数。实际中 很少单独使用原型链

借用构造函数

    借用构造函数 ,也成为 经典继承。即 在子类 构造函数的内部 调用 父类构造函数。函数 只不过是 在特定环境中执行的 代码的对象,因此通过 使用 apply()和 call() 方法 也可以在新建的对象上 执行构造函数。

JS高程(二)_第26张图片

    通过使用 call 或者 apply方法 ,我们实际上 在 新创建 的 subType 实例(instance1)的 环境下 调用了 SuperType 构造函数。 这样一来,就会在 SubType对象上 执行 SuperType()函数中定义的所有对象初始化代码。 结果,SubType 的 每个实例就都 具有了 colors 属性的副本了。

    解决 传递参数 的 问题

JS高程(二)_第27张图片

    为 确保 SuperType 构造函数 不会 重写 子类的属性, 可以 在 调用 父类的构造函数之后,在 添加 应该 在 子类中定义的属性

     借用构造函数的问题 : 仅仅是借用构造函数,那么就无法避免 构造函数模式 存在的问题--------方法都在构造函数中定义,函数 不能 复用。 在 父类原型 中定义的方法,子类都是不可见的,只能使用 构造函数 模式。 因此 ,借用 构造函数 的技术也是很少单独使用的。

组合继承

也称 伪经典继承,指的的将 原型链 和 借用构造函数 的 技术组合到一起。

JS高程(二)_第28张图片
组合继承

        组合继承 避免了 原型链 和 借用构造函数 的 缺陷,融合了 它们的优点, 成为 JS 中最常用 的 继承模式。而且 ,instanceof 和 isPrototype() 也能够 用于 识别 基于组合继承创建的对象。

  原型式继承

        原型式继承并没有使用严格意义上的构造函数,借助原型 可以 基于 已有的对象创建新对象, 同时 还不必 因此 创建 自定义类型。

JS高程(二)_第29张图片

        在 object 函数内部,先创建了一个临时性 的 构造函数,然后将传入的 对象作为这个构造函数的原型,最后 返回了 这个 临时类型 的 一个新实例。

        ECMAScript5 通过 新增 Object.create() 方法 规范了 原型式继承。 这个方法接受两个参数:一个用作 新对象原型  的对象 和 (可选)一个为新对象定义额外属性的对象。在传入 一个参数的情况下,Object.create() 与 object()方法 相同。

JS高程(二)_第30张图片
Object.create()

    Object.create() 的第二个参数 与 object.defieProperties() 方法 的 第二个参数格式相同:每个属性都是通过自己的描述符定义的。 以这种方式 指定的 任何属性都会 覆盖 原型对象上的 同名属性。

JS高程(二)_第31张图片

    在没 必要 兴师动众 地 创建构造函数,而 只想让 一个对象 与 另外一个对象保持类似 地情况下,原始式继承 式完全可以胜任地。但是,包含 引用类型的 属性 始终都会共享相应的值,就像使用 原型模式一样。

寄生式继承

    寄生式继承 与 寄生构造函数 和 工厂模式 类似,即 创建 一个仅用于 封装继承过程的函数,该函数在内部以 某种 方式来增强对象,最后再像 真 地是 它做了所有工作一样 返回对象。

JS高程(二)_第32张图片
object 函数 在上图

    在 主要 考虑 对象 而 不是 自定义类型 和 构造函数 的情况下,寄生式继承 也是一种游泳的模式。前面 示范 继承模式时 使用的 object()函数 不是必须的;任何 能够返回 新对象的 函数都 适用于 此模式。

寄生组合式继承

        组合继承是 JavaScript 最常用的 继承模式: 不过,它也有自己的不足。组合继承模式最大的问题 就是 无论 在什么情况下,都会调用 超过 两次 父类 构造函数: 一次 是在创建 子类型原型的 时候,另一次是在子类型 构造函数内部。

JS高程(二)_第33张图片
组合继承

        第一次调用 SuperType 构造函数时,SubType.prototype 会得到两个属性,name 和 colors , 它们都是 SuperType 的实例属性,只不过 位于SubType 的原型中,第二次调用SuperType,又在 新对象上 创建了 实例属性 name  和 colors 。 于是 ,这两属性 就屏蔽了原型中的两个同名属性。

        寄生组合模式,通过 借用 构造函数 来 继承属性,通过原型链的 混成形式 来继承方法,其基本思路 是: 不必为了指定 子类型的原型 而 调用 父类型的 构造函数,我们 所需要的 无非就是 父类型 原型 的一个副本而已。本质上 就是 使用 寄生式继承来继承 父类型的原型,然后再将结果 指定给 子类型的原型

JS高程(二)_第34张图片
object 是前文中的函数  

        inheritPrototype 函数接受 两个参数: 子类型构造函数 和 父类型构造函数。 在 函数内部,第一步是 创建 父类型原型的 一个副本;第二步 是为了创建的副本 添加 constructor 属性,从而 弥补 因类型重写 而 失去 默认的 constructor属性。第三步,将 新创建的对象(即副本)。这样,我们就可以用 inheritPrototype 函数 来 为子类型 原型赋值。

JS高程(二)_第35张图片

        这个例子 高效率体现在 它 之调用了一次 SuperType 构造函数,并且 因此避免了在 SubType.prototype 上创建不必要的、多余 属性。与此同时,原型链还能保持 不变,一次能正常 使用 instanceof  和 isPrototypeOf ()。 寄生组合式继承 是 引用类型最理想的继承方式

         总结:原型链的构建 是 通过 将一个类型的实例 赋值 给另外一个 构造函数的原型 实现的。 原型链的 问题 是 对象实例 共享所有继承的 属性 和 方法,因此不适合单独使用。解决这个问题的技术 是 借用构造函数,即在 子类构造函数的内部 调用 父类型构造函数。这样就可以做到每个实例 都具有自己的属性,同时还能保证 只使用构造函数模式来定义类型。使用最多的继承模式 是 组合继承。这种模式 使用 原型链 继承共享的 属性 和方法,而 通过借用构造函数继承实例属性。

函数表达式

    定义函数的方式有两种:一种是 函数声明,另一种是 函数表达式。

函数表达式 这种形式 好像常规的 变量赋值语句,即 创建一个函数并将它 赋值 给变量 ,这种情况下创建的 函数 叫 匿名函数(也叫 拉姆达函数),因为 function 关键字后面没有 标识符。

函数表达式

JS高程(二)_第36张图片
前者在不同的浏览器上会出现不同的结果,后者不会出现意外的情况  

递归

        递归 是在 一个函数通过名字调用自身的 情况下构成的。

JS高程(二)_第37张图片
 递归

    上面是一个 经典的递归阶乘函数,但是下面 代码 会导致 错误。

        将 factorial 设置 为 null 后,再调用 anotherFactorial, 而 函数 内部的 factorial 已经不是函数了,所以会直接报错, 在前文 中 我们 使用了 arguments. callee 来 代替 函数名

JS高程(二)_第38张图片

但在严格模式下, 不能使用 arguments. callee,可以使用 命名函数表达式 来达成相同的结果

JS高程(二)_第39张图片

    上述 代码 创建了一个 名为 f() 的命名函数表达式,f 只在 内部作用域有效, 外部 无法获取;然后将 它 赋值给变量 factorial,即使 把 函数赋值 给另外一个变量,函数的名字 f 仍然有效。

闭包

闭包 是指 有权访问 另一个 函数作用域 中的变量的 函数创建 闭包的常用方式,是在一个函数中 创建 另外一个 函数

        一般来讲,当函数执行完毕后,局部活动对象 就会被 销毁,内存中 仅 保存 全局作用域(全局执行环境的变量对象)。但是 闭包的 情况又有所不同。

         闭包中的活动对象 仍然会留在 内存中,知道 匿名函数 被销毁后,活动对象才会被销毁。

 闭包 与 变量

 闭包 只能 取得 包含 函数中任何变量的 最后一个值。

JS高程(二)_第40张图片
每个函数都是返回的 10  

        我们可以通过 创建一个 匿名函数 强制 让闭包的行为 符合预期

JS高程(二)_第41张图片

    我们并没有直接把 闭包 赋值给数组 ,而是 定义了一个匿名函数,并立即执行该匿名函数的结果赋值给数组。这里的 匿名函数 有个 参数 num, 也就是最终的 函数 要返回的值。在调用每个匿名函数时,我们传入 了变量 i。由于函数参数 是按值传递的,所以就会将变量 i 的当前值 复制给参数 num, 而这个匿名函数内部,又创建并返回了 一个 访问 num 的闭包。这样一来,result 数组 中的每个函数都有自己 num 变量的一个副本, 因此就可以返回不同的数值了。

        ES6中简单的办法 是将 for 循环 中的 var 改为 let.

 关于 this 对象

    在闭包中使用 this 对象 也可能会导致一些问题,this 对象 是在 运行时 基于 函数的执行环境绑定的:在 全局函数中,this 等于 window , 而当函数 被当作 某个对象的方法调用时,this 等于 那个对象。不过 , 匿名函数的执行环境 具有全局性,因此 其 this 对象通常指向 window(call 和 apply 能改变函数执行环境)。

JS高程(二)_第42张图片

        每个函数 在被调用时,其活动对象都会自动获取两个特殊变量: this  和 arguments。内部函数在搜索着两个变量时候,知乎搜索到其活动对象,因此 永远不可能直接访问外部 函数中的着这两个对象.

内存泄漏

        如果 闭包的作用域链中保存着一个 HTML 元素,那么意味着 该元素 无法被销毁;

JS高程(二)_第43张图片

    由于匿名函数 保存了 一对 assignHandle 活动对象的 引用, 因此 就会导致 无法减少 element 的引用数。只要匿名函数存在,element 的 引用数 至少是1,因此 它所占用的内存就永远不会被回收。

    下面代码,通过 element.id  的一个副本 保存在一个变量中,并且在闭包中引用该变量消除循环引用。但是 仅仅做到这一步,还是不能够解决内存泄漏的问题 。必须记住: 闭包会 引用 包含函数的整个活动对象,而 其中 包含着 element 。即使闭包不直接引用 element ,包含函数的活动对象中 也仍然会保存一个引用。因此 ,有必要 把element 变量设置 为 null 。 这样就能够解除 对 DOM 对象的引用,顺利地 减少其引用数,确保正常回收其占用的内存。

JS高程(二)_第44张图片

模仿块级作用域

        ES6 中已经由 块级作用域了。

无论什么地方,只要临时需要一个变量, 就可以使用 私有作用域

 在匿名函数中定义的任何变量,都会在执行结束时被销毁。因此,变量 i 只在循环中使用,使用后即被销毁

JS高程(二)_第45张图片

        这种技术经常在全局作用域中被使用在函数外部,从而限制 向全局作用域中添加过多的变量和函数。 一般来说,我们应该尽量 减少 向 全局作用域中 添加 变量 和 函数。 在一个由很多开发人员共同参与的 大型应用程序中,过多的全局变量 和 函数 很容易 导致 命名冲突。而通过创建私有作用域,每个 开发人员既 可以使用自己的变量,又不必担心 搞乱 全局作用域。

JS高程(二)_第46张图片

其中 变量 now 是 匿名函数额度局部变量,我们不必再全局作用域中创建它。这种做法可以 减少闭包占用的内存问题,因为 没有指向匿名函数 的 引用。而 我们不必在 全局作用域 中创建它。


 私有变量

        严格来讲, JavaScript 中没有私有成员的概念,所有对象属性都是公有的。 不过,倒是又一个 私有变量的概念。 任何在函数中 定义的变量,都可以认为是 私有变量,因为不能 在函数的外部访问这些变量私有变量包括 函数的参数、局部变量 和 在函数内部定义的 其他函数

        在这个函数内部,有 3 个 变量: num1、num2  和 sum。在函数内部可以访问这几个变量,但在函数外部则不能访问它们。如果在  这个函数内部创建一个闭包,那么 闭包通过自己的作用域链 也可以访问这些变量。利用这一点,就可以创建用于访问私有变量的公有方法。

我们把 有权访问 私有变量 和 私有函数 公共方法 称为 特权方法(privileged method)。有两种在 对象上 创建特权方法的 方式。

        第一种 是 在构造函数中定义特权方法:

JS高程(二)_第47张图片

这种模式 在 构造函数 内部定义了 所有 私有变量 和 函数。然后又继续 创建了能够 访问这些私有成员的 特权方法。能够在 构造函数 内部定义 特权方法,是因为 特权方法 作为 闭包 有权访问 构造函数中定义的所有变量和函数

 利用 私有 和 特权成员,可以 隐藏那些不应该被直接修改的数据。

JS高程(二)_第48张图片

构造函数中定义的两个 特权方法,这两个 方法都可以 在 构造函数外部使用,而且都有特权访问 私有变量 name。但在 person构造函数外部,没有任何办法访问 name。由于 这两个方法都是 在构造函数内部定义的,它们作为闭包能够通过 作用域链能够访问 name。私有变量name 在 Person 的 每一个实例中 都不同,因为 每次调用 构造函数 都会重新 创建这两个方法。不过,在构造 函数中 定义 特权方法 也有一个 缺点,那就是 你必须 使用构造函数模式来达到目的,构造函数模式 的缺点 是 针对每个实例都会创建同样一组新方法,而使用静态私有变量来实现 特权方法就可以避免这个问题。

   静态私有变量

        通过私有作用域中定义 私有变量 或 函数,同样也可以创建 特权方法。

JS高程(二)_第49张图片

    这个模式创建了一个私有作用域,并在其中封装了一个 构造函数 及 相应的方法。在私有作用域中, 首先定义了 私有变量 和 私有函数,然后 又定义了构造函数 及其公有方法。公有方法是 在原型上定义的,这一点体现了典型的原型模式。 需要注意的是,这个模式 在定义构造函数时 并没有使用函数声明,而是 使用了函数表达式。函数声明 只能创建 局部函数,但那并不是我们想要的。出于同样的原因 ,我们也没有 在声明 MyObject 时 使用 var 关键。记住: 初始化未经声明的变量,总是会创建一个全局变量。因此,MyObject  就成了 一个全局变量,能够在私有作用域 之外 被访问到。但在 严格模式 下, 给未经声明的变量赋值 会导致错误。

这个模式 与 构造函数 中定义 特权方法的主要区别,就 在于 私有变量 和 函数 是 由实例共享的。由于特权方法是在原型上定义的,因此所有实例都使用同一个函数。而这个 特权方法,作为一个闭包,总是保存着对包含作用域的引用。

JS高程(二)_第50张图片

这个例子中的 Person 构造函数与 getName()  和 setName() 方法一样,都有权访问私有变量name。在这种模式下, 变量 name 就会变成 一个 静态的、由 所有实例共享的 属性。也就是说,在一个实例上调用 setName()会 影响 所有 实例。 也就是说,在一个实例上 调用 setName() 会影响所有实例。而调用 setName()或新建一个 Person 实例 都会赋予 name 属性 一个新值。结果 所有的实例都会返回相同的结果。

        以这种方式 创建 静态私有变量 会因为 使用 原型 而 增进代码复用,但每个实例都没有自己的私有变量。到底 是 使用实例变量,还是静态私有变量,最终还是要 视你的 具体需求而定。

       多查找作用域链中的一个层次,就会 在一定程度上影响查找速度。而这正是 使用闭包和私有变量的一个明显的不足之处。


模块模式

    前面的 模式 是 用于 为了自定义类型创建 私有变量 和 特权方法。而 模块模式 则是为了单例 创建私有变量 和 特权方法。所谓单例,指的就是 一个实例的对象。按照惯例,Javascript 是以对象字面量的方式 来创建 单例对象的。

模块模式 通过 为 单例添加 私有变量 和 特权方法 能使其得到增强。

JS高程(二)_第51张图片

    这个模块模式 使用了 一个 返回对象 的 匿名函数,这个 匿名函数内部,首先定义了私有变量 和 函数。然后,将一个对象字面量 作为函数的返回值。返回的 对象字面量 只包含 可以公开的属性 和 方法。由于 这个对象事是 在匿名函数内部定义的,因此它的 公有方法 有权访问 私有变量 和 函数。从本质上来讲,这个 对象字面量定义的 是 单例的公共接口。这种模式 在需要对 单例进行某些初始化,同时又需要 维护其私有变量时 是 非常有用的。

JS高程(二)_第52张图片

    首先 声明了一个 私有的 components 数组,并 向数组 中 添加了 一个 BaseComponent 的 新实例(并不需要关心 BaseComponent 的 代码,我们只是用它来展示 初始化操作  )。而 返回对象的 getComponentCount() 和 registerComponent()方法,都有权访问数组 components 的特权方法。

    如果 必须 创建 一个对象,并 以某些数据 对其进行初始化,同时还要 公开一些能够访问这些私有数据的方法,那么就可以使用 模块模式。以这种模式 创建的每个单例都是 Object 的实例,因为最终要通过一个字面量来表示它。


增强的模块模式

    有人 进一步 改进了 模块模式,即在 返回对象之前 加入对其增强的代码。这种增强的模块模式适合那些 单例必须 是某种类型的实例,同时还 必须 添加某些 属性 和 (或)方法对其加以增强的情况。

JS高程(二)_第53张图片

            如果前面演示模块模式的例子中 的 application 对象 必须是 BaseComponent 的实例,那么可以使用 以下代码。

JS高程(二)_第54张图片

           在这个重写后的 应用程序 application 单例中,首先 也是 像前面例子中一样 定义了 私有变量。主要的不同之处 在于 命名变量 app 的创建过程,因为它 必须是 BaseComponent 的 实例。这个实例实际上 是 application 对象的局部变量版。此后我们又为 app 对象添加了能够访问私有变量的公有方法。最后一步是 返回 app 对象,结果仍然是将 它 赋值给全局变量 application。


   总结: 函数表达式不同于 函数声明。 函数声明要求又名字,但函数表达式 不需要。没有名字的函数表达式也叫 匿名函数;在无法确定如何引用函数的情况下,递归函数就会变得比较复杂;递归函数应该始终使用 arguments.callee 来递归地调用 自身,不要使用 函数名————函数名可能发生变化。

    闭包有权访问包含函数内部的所有变量,原理如下:

        1 在后台执行环境 中,闭包的作用域 包含着 它自己的作用域、包含函数的作用域 和 全局作用域。

        2 通常, 函数的作用域以及 其所有变量 都会在 函数 执行结束后 被销毁。

        3 但是,当函数返回 一个闭包时,这个函数的作用域 将会一直在内存中保存到闭包不存在为止。

        使用闭包 可以 在JS中 模仿块级作用域,要点如下:

        1 创建 并立即调用一个函数,这样既可以执行其中的代码,又不会在内存中留下对该函数的引用。

        2 结果 就是 函数内部的所有变量都会被 立即销毁—— 除非将某些变量赋值给了包含作用域(即外部作用域)中的变量。

        3 有权访问私有变量的公有方法叫做特权方法。

        4 可以使用构造函数、原型模式 来实现自定义类型的特权方法,也可以使用模块模式、增强模块模式来实现单利的特权方法。

    Javascript 中的函数表达式 和闭包都是极有用的特性,利用它们可以实现很多功能。不过,因为创建闭包 必须 维护额外的 作用域,所以过度使用它们可能会占用大量内存。

你可能感兴趣的:(JS高程(二))