你不知道的JS(上) note

164 阅读26分钟

你不知道的JS(上) note

作用域篇

  • 对于 JavaScript 来说,大部分情况下编译发生在代码执行前的几微秒(甚至更短!)的时 间内。在我们所要讨论的作用域背后,JavaScript 引擎用尽了各种办法(比如 JIT,可以延 迟编译甚至实施重编译)来保证性能最佳。

  • 引擎

    • 从头到尾负责整个 JavaScript 程序的编译及执行过程。
  • 编译器

    • 引擎的好朋友之一,负责语法分析及代码生成等脏活累活(详见前一节的内容)。
  • 作用域

    • 引擎的另一位好朋友,负责收集并维护由所有声明的标识符(变量)组成的一系列查 询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。
  • var a = 2;

    1. 遇到 var a,编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的 集合中。如果是,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作 用域的集合中声明一个新的变量,并命名为 a。
    2. 接下来编译器会为引擎生成运行时所需的代码,这些代码被用来处理 a = 2 这个赋值 操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作 a 的 变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变,
    3. 如果引擎最终找到了 a 变量,就会将 2 赋值给它。否则引擎就会举手示意并抛出一个异 常!
    4. 总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如 果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对 它赋值。
function foo(a) {
console.log( a ); // 2
}
foo( 2 );
  • 这里还有一个容易被忽略却非常重要的细节。 代码中隐式的 a=2 操作可能很容易被你忽略掉。这个操作发生在 2 被当作参数传递给 foo(..) 函数时,2 会被分配给参数 a。为了给参数 a(隐式地)分配值,需要进行一次 LHS 查询。

作用域嵌套

作用域是根据名称查找变量的一套规则

当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用 域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量, 或抵达最外层的作用域(也就是全局作用域)为止。

LHS 于 RHS的区别

function foo(a) {
console.log( a + b );
b = a;
}
foo( 2 );
  • 第一次对 b 进行 RHS 查询时是无法找到该变量的。也就是说,这是一个“未声明”的变 量,因为在任何相关的作用域中都无法找到它。 如果 RHS 查询在所有嵌套的作用域中遍寻不到所需的变量,引擎就会抛出 ReferenceError 异常。值得注意的是,ReferenceError 是非常重要的异常类型。
  • 相较之下,当引擎执行 LHS 查询时,如果在顶层(全局作用域)中也无法找到目标变量, 全局作用域中就会创建一个具有该名称的变量,并将其返还给引擎,前提是程序运行在非 “严格模式”下。
  • ES5 中引入了“严格模式”。同正常模式,或者说宽松 / 懒惰模式相比,严格模式在行为上 有很多不同。其中一个不同的行为是严格模式禁止自动或隐式地创建全局变量。因此,在 严格模式中 LHS 查询失败时,并不会创建并返回一个全局变量,引擎会抛出同 RHS 查询 失败时类似的 ReferenceError 异常。 接下来,如果 RHS 查询找到了一个变量,但是你尝试对这个变量的值进行不合理的操作, 比如试图对一个非函数类型的值进行函数调用,或着引用 null 或 undefined 类型的值中的 属性,那么引擎会抛出另外一种类型的异常,叫作 TypeError。
  • ReferenceError 同作用域判别失败相关,而 TypeError 则代表作用域判别成功了,但是对 结果的操作是非法或不合理的。

小结

作用域是一套规则,用于确定在何处以及如何查找变量(标识符)。如果查找的目的是对 变量进行赋值,那么就会使用 LHS 查询;如果目的是获取变量的值,就会使用 RHS 查询。

赋值操作符会导致 LHS 查询。=操作符或调用函数时传入参数的操作都会导致关联作用域 的赋值操作。

JavaScript 引擎首先会在代码执行前对其进行编译,在这个过程中,像 var a = 2 这样的声 明会被分解成两个独立的步骤:

  1. 首先,var a 在其作用域中声明新变量。这会在最开始的阶段,也就是代码执行前进行。
  2. 接下来,a = 2 会查询(LHS 查询)变量 a 并对其进行赋值。

LHS 和 RHS 查询都会在当前执行作用域中开始,如果有需要(也就是说它们没有找到所 需的标识符),就会向上级作用域继续查找目标标识符,这样每次上升一级作用域(一层 楼),最后抵达全局作用域(顶层),无论找到或没找到都将停止。 不成功的 RHS 引用会导致抛出 ReferenceError 异常。

不成功的 LHS 引用会导致自动隐式 地创建一个全局变量(非严格模式下),该变量使用 LHS 引用的目标作为标识符,或者抛 出 ReferenceError 异常(严格模式下)。

$词法作用域

作用域共有两种主要的工作模型。

  • 第一种是最为普遍的,被大多数编程语言所采用的词法 作用域
  • 另外一种叫作动态作用域,仍有一些编程语 言在使用(比如 Bash 脚本、Perl 中的一些模式等)。
  • 简单地说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写 代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域 不变(大部分情况下是这样的)。
  • 无论函数在哪里被调用,也无论它如何被调用,它的词法作用域都只由函数被声明时所处 的位置决定。
  • 词法作用域查找只会查找一级标识符,比如 a、b 和 c。如果代码中引用了 foo.bar.baz, 词法作用域查找只会试图查找 foo 标识符,找到这个变量后,对象属性访问规则会分别接 管对 bar 和 baz 属性的访问。

对象

对象是 JavaScript 的基础。

null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug,即对 null 执行 typeof null 时会返回字符串 ;

原理是这样的,不同的对象在底层都表示为二进制,在 JavaScript 中二进制前三位都为 0 的话会被判 断为 object 类型,null 的二进制表示是全 0,自然前三位也是 0,所以执行 typeof 时会返回“object”。

"object"。1 实际上,null 本身是基本类型。 有一种常见的错误说法是“JavaScript 中万物皆是对象”,这显然是错误的。

对于 简单原始类型,引擎自动把字面量转换成对象,所以可以访问属性和方法。

String, Number, Boolean 等

null 和 undefined 没有对于的构造形式。

  • 在对象中,属性名永远都是字符串。如果你使用 string(字面量)以外的其他值作为属性 名,那它首先会被转换为一个字符串。即使是数字也不例外,虽然在数组下标中使用的的 确是数字,但是在对象属性名中数字会被转换成字符串,所以当心不要搞混对象和数组中 数字的用法:

属性和方法

严格来说,对象中的函数不属于这个对象,只是这个调用这个函数所绑定的this值是变化的,所以严格来说,函数并不是方法

数组

你完全可以把数组当作一个普通的键 / 值对象来使用,并且不添加任何数值索引,但是这 并不是一个好主意。数组和普通的对象都根据其对应的行为和用途进行了优化,所以最好 只用对象来存储键 / 值对,只用数组来存储数值下标 / 值对。

复制对象

tip: toString() 来序列 化一个函数的源代码

属性描述符

Object.getOwnPropertyDescriptor

writable: false,

  • 不可写,非严格模式下静默失败,严格模式会报错;

configurable: false, 不管是不是处于严格模式,尝 试修改一个不可配置的属性描述符都会出错。

  • 注意:如你所见,把 configurable 修改成 false 是单向操作,无法撤销!
  • 除了无法修改,configurable:false 还会禁止删除这个属性

Enumerable:

  • 从名字就可以看出,这个描述符控制的是属性是否会出现在对象的属性枚举中,比如说 for..in 循环。如果把 enumerable 设置成 false,这个属性就不会出现在枚举中,虽然仍 然可以正常访问它。相对地,设置成 true 就会让它出现在枚举中。

不变性

  • 对象常量

    • 结合 writable:false 和 configurable:false 就可以创建一个真正的常量属性(不可修改、 重定义或者删除):
    • var myObject = {}; 
      Object.defineProperty( myObject, "FAVORITE_NUMBER", { value: 42, writable: false, configurable: false } )
      
  • 禁止扩展

    • 如 果 你 想 禁 止 一 个 对 象 添 加 新 属 性 并 且 保 留 已 有 属 性, 可 以 使 用 Object.prevent Extensions(..):
    • var myObject = {
      a:2
      };
      Object.preventExtensions( myObject );
      myObject.b = 3;
      myObject.b; // undefined
      
  • 密封

    • Object.seal(..) 会创建一个“密封”的对象,
    • 这个方法实际上会在一个现有对象上调用 object,preventExtensions() 并把现有所有属性标记为: configurable:false。
  • 冻结

    • Object.freeze(..) 会创建一个冻结对象,

    • 这个方法实际上会在一个现有对象上调用 Object.seal(..) 并把所有“数据访问”属性标记为 writable:false,这样就无法修改它们 的值。

      这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意 直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的)。 你可以“深度冻结”一个对象,具体方法为,首先在这个对象上调用 Object.freeze(..), 然后遍历它引用的所有对象并在这些对象上调用 Object.freeze(..)。但是一定要小心,因 为这样做有可能会在无意中冻结其他(共享)对象。

Get

var myObject = {
a: 2
};
myObject.a; // 2
  • 在语言规范中,myObject.a 在 myObject 上实际上是实现了 [[Get]] 操作(有点像函数调 用:[Get])。对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性, 如果找到就会返回这个属性的值。
  • 然而,如果没有找到名称相同的属性,按照 [[Get]] 算法的定义会执行另外一种非常重要 的行为: 遍历原型链

getter & setter

通常来说 getter 和 setter 是成对出现的(只定义一个的话 通常会产生意料之外的行为)

存在性

  • in 操作符会检查属性是否在对象及其 [[Prototype]] 原型链中
  • 相比之下, hasOwnProperty(..) 只会检查属性是否在 myObject 对象中,不会检查 [[Prototype]] 链。
  • 看起来 in 操作符可以检查容器内是否有某个值,但是它实际上检查的是某 个属性名是否存在。

对于 for...in

  • 如果对一个值进行enumerable: false, 操作,则不会出现在 for..in 循环中(尽管 可以通过 in 操作符来判断是否存在)。原因是“可枚举”就相当于“可以出现在对象属性 的遍历中”。
  • 在数组上应用 for..in 循环有时会产生出人意料的结果,因为这种枚举不 仅会包含所有数值索引,还会包含所有可枚举属性。最好只在对象上应用 for..in 循环,如果要遍历数组就使用传统的 for 循环来遍历数值索引。

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

区别

  • in 和 hasOwnProperty(..) 的区别在于是否查找 [[Prototype]] 链
  • 然而,Object.keys(..) 和 Object.getOwnPropertyNames(..) 都只会查找对象直接包含的属性。

遍历

  • 使用 for..in 遍历对象是无法直接获取属性值的,因为它实际上遍历的是对象中的所有可 枚举属性,你需要手动获取属性值。

遍历数组下标时采用的是数字顺序(for 循环或者其他迭代器),但是遍历对 象属性时的顺序是不确定的,在不同的 JavaScript 引擎中可能不一样。因此, 在不同的环境中需要保证一致性时,一定不要相信任何观察到的顺序,它们 是不可靠的。

  • for..of 循环首先会向被访问对象请求一个迭代器对象,然后通过调用迭代器对象的 next() 方法来遍历所有返回值。

  • 数组有内置的 @@iterator,因此 for..of 可以直接应用在数组上。

小结

JavaScript 中的对象有字面形式(比如 var a = { .. })和构造形式(比如 var a = new Array(..))。字面形式更常用,不过有时候构造形式可以提供更多选项。

许多人都以为“JavaScript 中万物都是对象”,这是错误的。对象是 6 个(或者是 7 个,取 决于你的观点)基础类型之一。对象有包括 function 在内的子类型,不同子类型具有不同 的行为,比如内部标签 [object Array] 表示这是对象的子类型数组。

你可以使用 ES6 的 for..of 语法来遍历数据结构(数组、对象,等等)中的值,for..of 会寻找内置或者自定义的 @@iterator 对象并调用它的 next() 方法来遍历数据值。

混合对象 ”类“

面向类的设计模式:实例化(instantiation)、继承(inheritance)和 (相对)多态(polymorphism)。

构造函数

类实例是由一个特殊的类方法构造的,这个方法名通常和类名相同,被称为构造函数。这 个方法的任务就是初始化实例需要的所有信息(状态)。

class CoolGuy {
    specialTrick = nothing
    CoolGuy( trick ) {
    specialTrick = trick
    }
    showOff() {
    output( "Here's my trick: ", specialTrick )
    }
}

注意,CoolGuy 类有一个 CoolGuy() 构造函数,执行 new CoolGuy() 时实际上调用的就是 它。构造函数会返回一个对象(也就是类的一个实例),之后我们可以在这个对象上调用 showOff() 方法,来输出指定 CoolGuy 的特长。

显式混入

  • 显式混入是 JavaScript 中一个很棒的机制,不过它的功能也没有看起来那么强大。虽然它 可以把一个对象的属性复制到另一个对象中,但是这其实并不能带来太多的好处,无非就 是少几条定义语句,而且还会带来我们刚才提到的函数对象引用问题。
  • 如果你向目标对象中显式混入超过一个对象,就可以部分模仿多重继承行为,但是仍没有 直接的方式来处理函数和属性的同名问题。有些开发者 / 库提出了“晚绑定”技术和其他的 一些解决方法,但是从根本上来说,使用这些“诡计”通常会(降低性能并且)得不偿失

寄生继承

//“传统的 JavaScript 类”Vehicle
function Vehicle() {
this.engines = 1;
}
Vehicle.prototype.ignition = function() {
console.log( "Turning on my engine." );
};
Vehicle.prototype.drive = function() {
this.ignition();
console.log( "Steering and moving forward!" );
};
​
//“寄生类”Car
function Car() {
// 首先,car 是一个 Vehicle
var car = new Vehicle();
// 接着我们对 car 进行定制
car.wheels = 4;
// 保存到 Vehicle::drive() 的特殊引用
var vehDrive = car.drive;
// 重写 Vehicle::drive()
car.drive = function() {
vehDrive.call( this );
console.log(
"Rolling on all " + this.wheels + " wheels!"
);
return car;
}
var myCar = new Car();
myCar.drive();
​
​

隐式混入

    var Something = {
            cool: function() {
              this.greeting = "Hello World";
              this.count = this.count ? this.count + 1 : 1;
            }
        };
        
    Something.cool();
        Something.greeting; // "Hello World"
        Something.count; // 1
​
        var Another = {
            cool: function() {
              // 隐式把Something混入Another
              Something.cool.call(this);
            }
        };
​
        Another.cool();
        Another.greeting; // "Hello World"
        Another.count; // 1(count不是共享状态)

小结

类是一种设计模式。许多语言提供了对于面向类软件设计的原生语法。JavaScript 也有类 似的语法,但是和其他语言中的类完全不同。

类意味着复制。

传统的类被实例化时,它的行为会被复制到实例中。

类被继承时,行为也会被复制到子类中。

多态看起来似乎是从子类引用父类,但是本质上引用的其实是复制的结果。

JS 并不会(像类那样)自动创建对象的副本。

混入(无论是显式还是隐士)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆弱的语法,这会让代码更加难以维护。

此外,显示混入实际上无法完全模拟类的复制行为,因为对象(和函数)只能复制引用,无法复制被引用的对象或者函数本身。

总的来说,在JS中模拟类是得不偿失的,虽然能解决当前的问题,但是可能会买下更多的隐患。

原型

[[Prototype]]

JavaScript 中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对于其他对象的引 用。几乎所有的对象在创建时 [[Prototype]] 属性都会被赋予一个非空的值。

当你试图引用对象的属性时会触发[[Get]] 操作,比如 myObject.a。对于默认的 [[Get]] 操作来说,第一步是检查对象本身是 否有这个属性,如果有的话就使用它。

但是如果 a 不在 myObject 中,就需要使用对象的 [[Prototype]] 链了。

使用 for..in 遍历对象时原理和查找 [[Prototype]] 链类似,任何可以通过原型链访问到 (并且是 enumerable)的属性都会被枚举。使用 in 操作符来检查属性在对象 中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举)

Object.prototype

所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype。由于所有的“普通”对象都“源于”

这个 Object.prototype 对象,所以它包含 JavaScript 中许多通用的功能。

属性设置和屏蔽

myObject.foo = "bar";

屏蔽比我们想象中更加复杂。

如果 foo 不直接存在于 myObject 中而是存在于原型链上层时 myObject.foo = "bar" 会出现的三种情况。

  • 如果在 [[Prototype]] 链上层存在名为 foo 的普通数据访问属性(参见第 3 章)并且没 有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 foo 的新 属性,它是屏蔽属性。
  • 如果在 [[Prototype]] 链上层存在 foo,但是它被标记为只读(writable:false),那么 无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
  • 如果在 [[Prototype]] 链上层存在 foo 并且它是一个 setter(参见第 3 章),那就一定会 调用这个 setter。foo 不会被添加到(或者说屏蔽于)myObject,也不会重新定义 foo 这 个 setter。

有些情况下会隐式产生屏蔽,一定要当心

var anotherObject = {
a:2
};
var myObject = Object.create( anotherObject );
anotherObject.a; // 2
myObject.a; // 2
/**/
anotherObject.hasOwnProperty( "a" ); // true
myObject.hasOwnProperty( "a" ); // false
myObject.a++; // 隐式屏蔽!
anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty( "a" ); // true

尽管 myObject.a++ 看起来应该(通过委托)查找并增加 anotherObject.a 属性,但是别忘 了 ++ 操作相当于 myObject.a = myObject.a + 1。因此 ++ 操作首先会通过 [[Prototype]] 查找属性 a 并从 anotherObject.a 获取当前属性值 2,然后给这个值加 1,接着用 [[Put]] 将值 3 赋给 myObject 中新建的屏蔽属性 a,天呐!

修改委托属性时一定要小心。如果想让 anotherObject.a 的值增加,唯一的办法是 anotherObject.a++。

“类”

为什么一个对象需要关联到另一个对象?这样做有什么好处?

  • JavaScript 和 面向类的语言不同,它并没有类来作为对象的抽象模式或者蓝图,JS中只有对象。
  • 实际上,JavaScript才是真正应该被称为 面向对象的语言,因为它是少有的可以不通过类直接创建对象的语言

  • 在JS中,类无法描述对象的行为,(因为根本就不存在类),对象直接定义自己的行为。
  • JavaScript 中只有对象

“类” 函数

多年以来,JavaScript 中有一种奇怪的行为一直在被无耻地滥用,那就是模仿类。

这种奇怪的“类似类”的行为利用了函数的一种特殊特性:所有的函数默认都会拥有一个 名为 prototype 的公有并且不可枚举的属性,它会指向另一个对象: Foo.prototype

最直接的解释就是,这个对象是在调用 new Foo()时创建的,最后会被 关联到这个“Foo 点 prototype”对象上。

调用 new Foo() 时会创建 a, 其中的一步就是给 a 一个内部 的 [[Prototype]] 链接,关联到 Foo.prototype 指向的那个对象。

new 关键字会进行如下操作:

  1. 创建一个空的JS对象({});
  2. 为这个新的对象添加属性 _ proto _ , 该属性关联到构造函数的原型对象(即:prototype);
  3. 将this指向这个新的对象;
  4. 如果函数没有返回对象,则返回 this

在 JavaScript 中,并没有类似的复制机制。你不能创建一个类的多个实例,只能创建 多个对象,它们 [[Prototype]] 关联的是同一个对象。但是在默认情况下并不会进行复制, 因此这些对象之间并不会完全失去联系,它们是互相关联的。

new Foo() 会生成一个新对象(我们称之为 a),这个新对象的内部链接 [[Prototype]] 关联 的是 Foo.prototype 对象。

最后我们得到了两个对象,它们之间互相关联,就是这样。我们并没有初始化一个类,实 际上我们并没有从“类”中复制任何行为到一个对象中,只是让两个对象互相关联。

实际上,绝大多数 JavaScript 开发者不知道的秘密是

new Foo() 这个函数调用实际上并没 有直接创建关联,这个关联只是一个意外的副作用。

new Foo() 只是间接完成了我们的目 标:一个关联到其他对象的新对象。

关于名称

“继承”这个词会让人产生非常强的心理预期。

仅仅在前面加上“原型”并 不能区分出 JavaScript 中和类继承几乎完全相反的行为,因此在过去 20 年中造成了极大的 误解。

继承意味着复制操作,JavaScript(默认)并不会复制对象属性。相反,JavaScript 会在两 个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。

委托 这个术语可以更加准确地描述 JavaScript 中对象的关联机制。

“构造函数”

到底是什么让我们认为 Foo 是一个“类”呢?

  • 其中一个原因是我们看到了关键字 new,在面向类的语言中构造类实例时也会用到它。
  • 一个原因是,看起来我们执行了类的构造函数方法,Foo() 的调用方式很像初始化类时类 构造函数的调用方式。

构造函数还是调用

实际上,Foo 和你程序中的其他函数没有任何区别。函数本身并不是构造函数,然而,当 你在普通的函数调用前面加上 new 关键字之后,就会把这个函数调用变成一个“构造函数 调用”。

实际上,new 会劫持所有普通函数并用构造对象的形式来调用它。

在 JavaScript 中对于“构造函数”最准确的解释是,所有带 new 的函数调用。

函数不是构造函数,但是当且仅当使用 new 时,函数调用会变成“构造函数调用”。

(原型)继承

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 = Object.create(Foo.prototype);

Bar.prototype.myLabel = function() {
    return this.label;
}

var a = new Bar('a', 'obj a')

console.log(a.myName());
console.log(a.myLabel());

这段代码的核心部分就是语句 Bar.prototype = Object.create( Foo.prototype )。调用 Object.create(..) 会凭空创建一个“新”对象并把新对象内部的 [[Prototype]] 关联到你 指定的对象(本例中是 Foo.prototype)。

换句话说,这条语句的意思是:“创建一个新的 Bar.prototype 对象并把它关联到 Foo. prototype”。

检查“类”关系

假设有对象 a,如何寻找对象 a 委托的对象呢?

  • 在传统的面向类环境中,检查一个实例(JavaScript 中的对象)的继承祖先(JavaScript 中的委托关联)通常被称为 内省(或者反射)。

  • instanceof Foo; // true
    
  • instanceof 操作符的左操作数是一个普通的对象,右操作数是一个函数。

  • instanceof 回答 的问题是:在 a 的整条 [[Prototype]] 链中是否有指向 Foo.prototype 的对象?

    • 可惜,这个方法只能处理对象(a)和函数(带 .prototype 引用的 Foo)之间的关系。如 果你想判断两个对象(比如 a 和 b)之间是否通过 [[Prototype]] 链关联,只用 instanceof 无法实现。

第二种判断 [[Prototype]] 反射的方法,它更加简洁:

Foo.prototype.isPrototypeOf( a ); // true
  • **isPrototypeOf()** 方法用于测试一个对象是否存在于另一个对象的原型链上。
  • isPrototypeOf(..) 回答的问题是:在 a 的整 条 [[Prototype]] 链中是否出现过 Foo.prototype ?

绝大多数(不是所有!)浏览器也支持一种非标准的方法来访问内部 [[Prototype]] 属性

  • a.__proto__ === Foo.prototype; // true
    
  • 这个奇怪的 .proto(在 ES6 之前并不是标准!)属性“神奇地”引用了内部的 [[Prototype]] 对象,如果你想直接查找(甚至可以通过 .proto.ptoto... 来遍历) 原型链的话,这个方法非常有用。

和我们之前说过的 .constructor 一样,.proto 实际上并不存在于你正在使用的对象中 (本例中是 a)。实际上,它和其他的常用函数(.toString()、.isPrototypeOf(..),等等)一样,存在于内置的 Object.prototype 中。

对象关联《原型链》

现在我们知道了,[[Prototype]] 机制就是存在于对象中的一个内部链接,它会引用其他 对象。

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

创建关联

那 [[Prototype]] 机制的意义是什么呢?为什么 JavaScript 开发者费这么大的力气(模拟 类)在代码中创建这些关联呢?

Object.create(..) 会创建一个新对象(bar)并把它关联到我们指定的对象(foo),这样 我们就可以充分发挥 [[Prototype]] 机制的威力(委托)并且避免不必要的麻烦(比如使 用 new 的构造函数调用会生成 .prototype 和 .constructor 引用)。

Object.create(null) 会 创 建 一 个 拥 有 空( 或 者 说 null)[[Prototype]] 链接的对象,这个对象无法进行委托。由于这个对象没有原型链,所以 instanceof 操作符(之前解释过)无法进行判断,因此总是会返回 false。 这些特殊的空 [[Prototype]] 对象通常被称作“字典”,它们完全不会受到原 型链的干扰,因此非常适合用来存储数据。

小结

如果要访问对象中不存咋一个属性,[[GET]]操作就会查找对象内部[[Prototype]]关联的对象。这个关联关系实际上定义了一条”原型链“,在查找属性时会对它进行遍历。

所有的普通对象都有内置的 Object.prototype,指向原型链的顶端(比如说全局作用域),如 果在原型链中找不到指定的属性就会停止。toString()、valueOf() 和其他一些通用的功能 都存在于 Object.prototype 对象上,因此语言中所有的对象都可以使用它们。

关联两个对象最常用的方法是使用 new 关键词进行函数调用,在调用的 4 个步骤)中会创建一个关联其他对象的新对象。

使用 new 调用函数时会把新对象的 .prototype 属性关联到“其他对象”。带 new 的函数调用 通常被称为“构造函数调用”,尽管它们实际上和传统面向类语言中的类构造函数不一样。

虽然这些 JavaScript 机制和传统面向类语言中的“类初始化”和“类继承”很相似,但 是 JavaScript 中的机制有一个核心区别,那就是不会进行复制,对象之间是通过内部的 [[Prototype]] 链关联的。

出于各种原因,以“继承”结尾的术语(包括“原型继承”)和其他面向对象的术语都无 法帮助你理解 JavaScript 的真实机制(不仅仅是限制我们的思维模式)。

相比之下,“委托”是一个更合适的术语,因为对象之间的关系不是复制而是委托。

行为委托

[[Prototype]] 机制就是指对象中的一个内部链接引用 另一个对象。

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

换句话说,JavaScript 中这个机制的本质就是对象之间的关联关系。

这个观点对于理解本章的内容来说是非常基础并且非常重要的。

在 JavaScript 中,[[Prototype]] 机制会把对象关联到其他对象。无论你多么努力地说服自 己,JavaScript 中就是没有类似“类”的抽象机制。

委托行为意味着某些对象(XYZ)在找不到属性或者方法引用时会把这个请求委托给另一 个对象(Task)。

这是一种极其强大的设计模式,和父类、子类、继承、多态等概念完全不同。在你的脑海中 对象并不是按照父类到子类的关系垂直组织的,而是通过任意方向的委托关联并排组织的。

比较思维模型

典型的(“原型”)面向对象风格:

function Foo(who) {
this.me = who;
}
Foo.prototype.identify = function() {
return "I am " + this.me;
};
function Bar(who) {
Foo.call( this, who );
}
Bar.prototype = Object.create( Foo.prototype );
Bar.prototype.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = new Bar( "b1" );
var b2 = new Bar( "b2" );
b1.speak();
b2.speak();

对象关联风格:

Foo = {
init: function(who) {
this.me = who;
},
identify: function() {
return "I am " + this.me;
}
};
Bar = Object.create( Foo );
Bar.speak = function() {
alert( "Hello, " + this.identify() + "." );
};
var b1 = Object.create( Bar );
b1.init( "b1" );
var b2 = Object.create( Bar );
b2.init( "b2" );
b1.speak();
b2.speak();

\