JavaScript 原型

原型

[[Prototype]]

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

【作用】:在引用对象属性时会触发 [[Get]] 操作,这在进行行为委托时非常有用(请参考行为委托)。

【注意】:

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

// 创建一个关联到 anotherObject 的对象
var myObject = Object.create(anotherObject);

for(var k in myObject) {
    console.log("found:" + k);
}
// found: a

("a" in myObject); // true

Object.prototype

何处才是 [[Prototype]] 的尽头呢?

所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype。

属性设置和屏蔽

【示例】:

myObject.foo = "bar";

【过程】:

  • myObject 对象中包含名为 foo 的普通数据访问属性:修改已有的属性值。
  • 若不包含,则遍历 [[Prototype]] 链(类似 [[Get]] 操作):
    • 原型链上不存在:foo 属性就会被直接添加到 myObject 上。
    • 原型链上存在,则:
      • 存在名为 foo 的普通数据访问属性,且没有被标记为只读:直接在 myObject 中添加一个名为 foo 的新属性。
      • 存在名为 foo 的属性,且被标记为只读:无法修改已有属性或者在 myObject 上创建 foo 属性。如果运行在严格模式下,代码会抛出一个错误。
      • 存在名为 foo 的属性,且是一个 setter:调用该 setter,且 foo 不会被添加到 myObject,也不会重新定义 foo 这个 setter。

【注意】:

  1. [[Get]] 和 [[Set]] 总是会选择原型链中最底层的同名属性。
  2. 如果你希望在任何情况下,都能通过创建属性或函数来屏蔽原型链上的同名属性和函数,那么就不能使用 = 操作符来赋值,而是使用 Object.defineProperty()。
  3. 原型链上的函数被屏蔽之后,就不得不使用显式伪多态。通常来说,使用屏蔽得不偿失,所以应当尽量避免使用。

【示例】:隐式屏蔽。

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++ === myObject.a = myObject.a + 1。因此 ++ 操作会首先通过 [[Prototype]] 查找属性 a 并从 anotherObject.a 获取当前属性值2,然后给这个值加 1。此时就执行 [[Put]] 操作,其过程如上所说,因此直接在 myObject 创建 a 属性,并为其赋值 3。

【建议】:在修改对象属性时,一定要明确该属性是当前对象本身的属性还是委托属性。如果是委托属性,就去修改委托对象上的该属性。

“类”

JavaScript 和面向类的语言不同,它并没有类来作为对象的抽象模式或者说蓝图。相对面向类语言,JavaScript 中只有对象(没有类)。

实际上,JavaScript 才是真正应该被称为“面向对象”的语言,因为它是少有的可以不通过类来直接创建对象的语言,并且由对象自己定义自己的行为。

“类”函数

【函数特殊特性】:所有的函数默认都会拥有一个名为 prototype 的公共且不可枚举的属性,它会指向另一个对象,这个对象通常被称为函数的原型。

【示例】:

function Foo() {
// ...
}

var a = new Foo();
Object.getPrototypeOf(a) === Foo.prototype; // true

【new 操作过程】:

  1. 创建一个空对象 object。
  2. 将 object 的 prototype 指向函数的 prototype。
  3. 将 this 指向 object。
  4. 执行函数体内的操作(将函数与属性绑定到 object 上)。
  5. 返回 object。
function People(name) {
    this.name = name;
    this.say = function() {
        console.log("I am " + this.name);
    }
}

var spirit = new People("spirit");
spirit.say();

// 等价于
function People(name) {
    var object = {};
    object.__proto__ = Peolple.prototype;
    object.name = name;
    object.say = function() {
        console.log("I am " + this.name);
    }
    return object;
}

var spirit = People("spirit");
spirit.say();
  • 在面向类的语言中,类可以被复制(或者说实例化)多次,就像用模具制作东西一样。之所以会这样是因为实例化(或者继承)一个类就意味着“把类的行为复制到物理对象中”,对于每一个新实例来说都会重复这个过程。
  • 但是在 JavaScript 中没有类似的复制机制。开发者不能创建一个类的多个实例,只能创建多个对象,它们 [[prototype]] 关联的是同一个对象。但是在默认情况下并不会进行复制,因此这些对象之间并不会完全失去联系,它们是互相关联的。
关于名称

在 JavaScript 中,我们不会将一个对象复制到另一个对象,只是将它们关联起来。

原型继承这个名称容易让人误解为动态语言版本的类继承。然而其实际的运行机制和类继承几乎完全相反。

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

“构造函数”

【示例】:

function Foo() {
    // ...
}

var a = new Foo();

【问】:到底是什么原因让我们会认为 Foo 是一个“类”呢?

【答】:

  1. 看到了关键字 new,在面向类的语言中构造类实例时也会用到这个关键字。
  2. Foo() 的调用方式很像初始化类时类构造函数的调用方式。

那么 JavaScript 中的“new Foo()”到底是什么意思呢?(参考构造函数还是调用)。

【示例】:

function Foo() {
    // ...
}

Foo.prototype.constructor === Foo; // true

var a = new Foo();
a.constructor === Foo; // true

【解释】:Foo.prototype 默认有一个公有并且不可枚举的属性 .constructor,这个属性引用的是对象关联的函数。

【注意】:由于 a.constructor === Foo,可能会让许多人误解 a 是由 Foo 构造的结果。然而实际上 a 本身没有 .constructor 属性,具体原因请参考之后原型链相关知识点。

Foo.prototype 的 .constructor 属性只是 Foo 函数在声明时的默认属性。如果你创建了一个新对象并替换了函数默认的 .prototype 对象引用,那么新对象并不会自动获得 .constructor 属性。

【示例】:

function Foo() { /*...*/ }

//创建一个新原型对象
Foo.prototype = { /*...*/ };

var a = new Foo();
a.constructor === Foo;    // false!
a.constructor === Object; // true!

【解释】:a 没有 .constructor 属性,它会委托原型链上的 Foo.prototype。但是这个对象是我们自行创建的新对象,它也没有 .constructor 属性,所以它会继续委托。这次会委托给原型链上顶端的 Object.prototype。这个对象有 .constructor 属性,指向内置的 Object() 函数。

【示例】:手动添加 .constructor 属性。

function Foo() { /*...*/ }

//创建一个新原型对象
Foo.prototype = { /*...*/ };

Object.defineProperty(Foo.prototype, "constructor", {
    enumerable: false,
    writable: true,
    configurable: true,
    value: Foo // 让 .constructor 指向 Foo
});
构造函数还是调用

“构造函数”和普通函数没有任何区别,函数本身并非构造函数。然而,在普通函数的函数调用前加上 new 关键字之后,就会把这个函数变成一个“构造函数调用”。

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

【总结】:

  1. 对于“构造函数”最准确的解释是,所有带 new 的函数调用。
  2. 函数不是构造函数,但是当且仅当使用 new 时,函数调用会变成“构造函数调用”。

(原型)继承

【对象关联时容易出现的错误-1】:

Foo.prototype = Bar.prototype;

【解释】:Foo.prototype = Bar.prototype 不会创建一个关联到 Bar.prototype 的新对象,它只是让 Foo.prototype 直接引用 Bar.prototype 对象。

【对象关联时容易出现的错误-2】:

Foo.prototype = new Foo();

【解释】:Foo.prototype = new Foo() 的确会创建一个关联到 Foo.prototype 的新对象。但是它使用了 Foo() 的“构造函数调用”,如果函数中有其他操作(例如写日志、修改状态、注册到其他对象、给 this 添加数据属性等等)的话,会影响关联到 Foo() 的其他对象。

【最好的做法】:使用 Object.create()。这样做唯一的缺点就是需要创建一个新对象然后把旧对象抛弃掉,不能直接修改已有的默认对象。

在 ES6 之前,开发者只能用过设置 .proto 属性来实现修改对象的 [[Prototype]] 关联。这个方法不是标准且无法兼容所有浏览器。ES6 添加了辅助函数 Object.setPrototypeOf(),可以用标准并且可靠的方法来修改关联。

【示例】:

// ES6 之前需要抛弃默认的 Foo.prototype 对象
Foo.prototype = Object.create(Foo.prototype);

// ES6 开始可以直接修改现有的 Foo.prototype
Object.setPrototypeOf(Foo.prototype, Bar.prototype);

.proto 实际上并不存在于正在使用的对象中,它真正存在的位置在 Object.prototype 中。虽然 .proto 看起来很像一个属性,但是实际上它更像一个 getter/setter。

【大致原理】:

Object.definePrototype(Object.prototype, "__proto__", {
    get: function() {
        return Objecy.getPrototypeOf(this);
    },
    set: function(o) {
        // ES6 中的 setPrototypeOf()
        Object.setPrototypeOf(this, o);
        return o;
    }
});

【解释】:因此,访问 obj.proto 时,实际上是调用了 obj.proto()(调用 getter 函数)。虽然 getter 函数存在于 Object.prototype 对象中,但是它的 this 指向对象 obj。

检查“类”关系

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

【方法一】:判断对象与函数。

a instanceof Foo;

【解释】:instanceof 操作符的左操作数是一个普通的对象,右操作数是一个函数。instanceof 回答的问题是:在 a 的整条 [[Prototype]] 链中是否有指向 Foo.prototype 的对象。

【注意】:这个方法只能处理对象和函数之间的关系。

【方法二】:

Foo.prototype.isPrototypeOf(a);
// b 是否出现在 C 的原型链中
b.isPrototypeOf(c);

【解释】:isPrototypeOf 回答的问题与 instanceof 相同,但不需要间接引用函数(Foo),它的 .prototype 属性会被自动访问。

【注意】:这个方法并不需要使用函数,直接使用 b 和 c 之间的对象引用来判断它们的关系。

对象关联

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

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

创建关联

Object.create() 会创建一个新对象,并将其关联到指定的对象上。

【优势】:充分发挥 [[prototype]] 机制的威力,并且避免不必要的麻烦(使用 new 的构造函数调用会生成 .prototype 和 .constructor 引用)。

【字典】:特殊的空 [[prototype]] 对象。因为这些对象完全不会受到原型链的干扰(无法进行委托),因此非常适合用来存储数据。

var dic = Object.create(null);
Object.create() 的 polyfill 代码

Object.create() 是在 ES5 中新增的函数,所以在 ES5 之前的环境中(比如旧版 IE)如果要支持这个功能的话就需要使用一段简单的 polyfill 代码,部分实现 Object.create() 的功能。

【polyfill 代码】:

if(!Object.create) {
    Object.create = function(o) {
        function F() {}
        F.prototype = o;
        return new F();
    };
}

【解释】:先使用一个一次性函数 F,再通过改写该函数的 .prototype 属性使其指向要关联的对象,最后再使用构造函数调用的方式来创建一个新对象进行关联。

【部分模拟的原因】:标准 ES5 中内置的 Object.create() 函数提供了一系列附加功能(例如属性描述符),而 ES5 之前的版本不支持这些功能,所以 polyfill 代码只能部分模拟。

关联关系是备用

【观点】:“对象之间的关联关系是处理“缺失”属性或者函数时的一种备用选项。”这个说法有点道理,但是并不是 [[prototype]] 的本质。

【示例】:

var anotherObject = {
    cool: function() {
        console.log("cool!");
    }
};

var myObject = Object.create(anotherObject);
myObject.cool(); // 输出:cool!

【解释】:由于存在 [[prototype]] 机制,这段代码可以正常工作。但是如果你这样写只是为了让 myObject 在无法处理属性或者函数时可以使用备用的 anotherObject,那么你的软件就会变得有些难以理解和维护。

【建议】:尽量不要选择备用这种设计模式。

【解决】:

var anotherObject = {
    cool: function() {
        console.log("cool!");
    }
};

var myObject = Object.create(anotherObject);

myObject.doCool = function() {
    this.cool(); // 内部委托!
};

myObject.doCool(); // 输出:cool!

【解释】:内部委托比起直接委托,可以让 API 接口设计更加清晰。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,265评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,078评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,852评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,408评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,445评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,772评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,921评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,688评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,130评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,467评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,617评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,276评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,882评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,740评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,967评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,315评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,486评论 2 348

推荐阅读更多精彩内容