本文深入解析JavaScript原型链机制,通过实例说明属性共享与独立控制的原理。详细对比原始模式、工厂模式、构造函数、原型模式及混合模式等五种对象创建方式,重点讲解组合继承与寄生组合式继承的实现细节与优缺点,帮助开发者理解内存优化与实例隔离的最佳实践。
原型链机制与属性搜索原理
理解JavaScript的原型链机制往往让人感到困惑,网络上相关的参考资料也层出不穷。每当夜深人静难以入眠时,阅读关于原型链和闭包的文章似乎成了助眠良方,效果出奇地好。
function Person (name) { this.name = name; }
function Mother () { }
Mother.prototype = { //Mother的原型
age: 18,
home: ['Beijing', 'Shanghai']
};
Person.prototype = new Mother(); //Person的原型为Mother
//用chrome调试工具查看,提供了__proto__接口查看原型
var p1 = new Person('Jack'); //p1:'Jack'; __proto__:18,['Beijing','Shanghai']
var p2 = new Person('Mark'); //p2:'Mark'; __proto__:18,['Beijing','Shanghai']
p1.age = 20;
/* 实例不能改变原型的基本值属性,正如你洗剪吹染黄毛跟你妈无关
* 在p1实例下增加一个age属性的普通操作,与原型无关。跟var o{}; o.age=20一样。
* p1:下面多了个属性age,而__proto__跟 Mother.prototype一样,age=18。
* p2:只有属性name,__proto__跟 Mother.prototype一样
*/
p1.home[0] = 'Shenzhen';
/* 原型中引用类型属性的共享,正如你烧了你家,就是烧了你全家的家
* 这个先过,下文再仔细唠叨一下可好?
* p1:'Jack',20; __proto__:18,['Shenzhen','Shanghai']
* p2:'Mark'; __proto__:18,['Shenzhen','Shanghai']
*/
p1.home = ['Hangzhou', 'Guangzhou'];
/* 其实跟p1.age=20一样的操作。换成这个理解: var o{}; o.house=['big','house']
* p1:'Jack',20,['Hangzhou','Guangzhou']; __proto__:18,['Shenzhen','Shanghai']
* p2:'Mark'; __proto__:18,['Shenzhen','Shanghai']
*/
delete p1.age;
/* 删除自定义的属性之后,原本被覆盖的原型值就重见天日了。这里就是向上搜索机制,所以才有下面的动态性
* p1:'Jack',['Hangzhou','Guangzhou']; __proto__:18,['Shenzhen','Shanghai']
* p2:'Mark'; __proto__:18,['Shenzhen','Shanghai']
*/
Person.prototype.lastName = 'Jin';
/* 改写原型,动态反应到实例中。正如你妈变新潮了,邻居提起你都说是潮妇的儿子
* 注意,这里我们改写的是Person的原型,就是往Mother里加一个lastName属性,等同于Mother.lastName='Jin'
* 这里并不是改Mother.prototype,改动不同的层次,效果往往会有很大的差异。
* p1:'Jack',['Hangzhou','Guangzhou']; __proto__:'jin';__proto__:18,['Shenzhen','Shanghai']
* p2:'Mark'; __proto__:'jin';__proto__:18,['Shenzhen','Shanghai']
*/
Person.prototype = {
age: 28,
address: { country: 'USA', city: 'Washington' }
};
var p3 = new Person('Obama');
/* 重写原型!这个时候Person的原型已经完全变成一个新的对象了,也就是说Person换了个娘。
* 换成这样理解:var a=10; b=a; a=20; c=a。所以b不变,变得是c,所以p3变化了,与Mother无关。
* p1:'Jack',['Hangzhou','Guangzhou']; __proto__:'jin';__proto__:18,['Shenzhen','Shanghai']
* p2:'Mark'; __proto__:'jin';__proto__:18,['Shenzhen','Shanghai']
* p3:'Obama';__proto__: 28 {country: 'USA', city: 'Washington'}
*/
Mother.prototype.no = 9527;
/* 改写原型的原型,动态反应到实例中。正如你妈他妈变新潮了,邻居提起你都说你丫外婆真潮
* 注意,这里我们改写的是Mother.prototype,p1p2会变,但上面p3跟Mother已经了无瓜葛了,不影响他。
* p1:'Jack',['Hangzhou','Guangzhou']; __proto__:'jin';__proto__:18,['Shenzhen','Shanghai'],9527
* p2:'Mark'; __proto__:'jin';__proto__:18,['Shenzhen','Shanghai'],9527
* p3:'Obama';__proto__: 28 {country: 'USA', city: 'Washington'}
*/
Mother.prototype = {
car: 2,
hobby: ['run','walk']
};
var p4 = new Person('Tony');
/* 重写原型的原型!这个时候Mother的原型已经完全变成一个新的对象了!
* 由于上面Person与Mother已经断开联系了,这时候Mother怎么变已经不影响Person了。
* p4:'Tony';__proto__: 28 {country: 'USA', city: 'Washington'}
*/
Person.prototype = new Mother(); //再次绑定
var p5 = new Person('Luffy');
// 这个时候如果需要应用这些改动的话,那就要重新将Person的原型绑到mother上了
// p5:'Luffy';__proto__: 2, ['run','walk']
p1.__proto__.__proto__.__proto__.__proto__ //null,你说原型链的终点不是null?
Mother.__proto__.__proto__.__proto__ //null,你说原型链的终点不是null? 经过上述基础内容的梳理,相信读者对原型链的基本概念已有了初步的认知。
接下来,我们需要深入探讨 p1.age = 20、p1.home = ['Hangzhou', 'Guangzhou'] 以及 p1.home[0] = 'Shenzhen' 这三者之间的区别。其中,p1.home[0] = 'Shenzhen' 这种操作形式,可以概括为 p1.object.method 或 p1.object.property 的访问模式。
对于 p1.age = 20 和 p1.home = ['Hangzhou', 'Guangzhou'] 这两行代码,其逻辑相对直观。我们可以暂时忽略原型链的概念,回想一下平时是如何为一个普通对象添加属性的。
var obj = new Object();
obj.name='xxx';
obj.num = [100, 200]; 这样理解是否更加清晰?其原理与常规的对象属性赋值并无二致。
那么,为什么 p1.home[0] = 'Shenzhen' 不会在 p1 实例中直接创建一个名为 home 的数组属性并设置其首元素为 'Shenzhen' 呢?我们不妨先搁置原型链,回顾前面的 obj 对象示例。如果尝试执行 var obj.name = 'xxx' 或 obj.num = [100, 200],在 obj 尚未定义的情况下,显然会报错,无法得到预期结果。同理,由于 p1 下并未预先定义 home 属性,因此无法直接通过 p1.home[0] 一步到位地定义 home 数组。若要在 p1 实例中创建 home 数组,正确的写法应当是:
p1.home = [];
p1.home[0] = 'Shenzhen'; 这是否正是我们日常开发中最常用的标准做法?
p1.home[0] = 'Shenzhen' 之所以没有直接报错,是因为原型链中存在一套搜索机制。当访问 p1.object 时,引擎会先在实例 p1 中查找该属性或方法;若未找到,则沿着原型链向上查找;若仍找不到,则继续向上一级原型搜索,直至到达原型链的终点(即 null)。如果最终未找到,则返回 undefined。同理,当执行 p1.home[0] 时,引擎先搜索 p1 实例中是否存在名为 home 的属性或方法,然后逐级向上查找。最终在 Mother 的原型中找到了 home,因此对 p1.home[0] 的修改实际上等同于修改了 Mother 的原型。
简而言之:p1.home[0] = 'Shenzhen' 等同于 Mother.prototype.home[0] = 'Shenzhen'。
原型继承的改良方案:组合与寄生组合
基于上述分析,原型链继承的主要缺陷在于属性的共享问题。在许多场景下,我们仅希望共享方法,而不希望共享属性,理想状态下每个实例应拥有独立的属性。为此,原型继承衍生出了以下两种改良方案:
第一种方案为组合继承。
function Mother (age) {
this.age = age;
this.hobby = ['running','football']
}
Mother.prototype.showAge = function () {
console.log(this.age);
};
function Person (name, age) {
Mother.call(this, age); //第二次执行
this.name = name;
}
Person.prototype = new Mother(); //第一次执行
Person.prototype.constructor = Person;
Person.prototype.showName = function () {
console.log(this.name);
}
var p1 = new Person('Jack', 20);
p1.hobby.push('basketball'); //p1:'Jack'; __proto__:20,['running','football']
var p2 = new Person('Mark', 18); //p2:'Mark'; __proto__:18,['running','football'] 其运行结果如下所示:


第一次执行时,得到 Person.prototype.age = undefined 和 Person.prototype.hobby = ['running','football'];第二次执行 var p1 = new Person('Jack', 20) 时,得到 p1.age = 20 和 p1.hobby = ['running','football'],执行 push 操作后 p1.hobby 变为 ['running','football', 'basketball']。只要理清 this 指向的变化,理解起来并不复杂,只需简单替换 this 即可推导结果。若觉得概念抽象,不妨摒弃脑海中的固有概念,模拟浏览器从上到下逐行执行代码,结果自然显现。
通过第二次执行原型的构造函数 Mother(),我们在对象实例中复制了一份原型的属性,从而实现了与原型属性的分离和独立。细心的读者可能会发现,第一次调用 Mother() 似乎并未产生实际作用,能否省略该调用?答案是可以的,这就引出了下面的寄生组合式继承。
第二种方案为寄生组合式继承。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function inheritPrototype(Person, Mother){
var prototype = object(Mother.prototype);
prototype.constructor = Person;
Person.prototype = prototype;
}
function Mother (age) {
this.age = age;
this.hobby = ['running','football']
}
Mother.prototype.showAge = function () {
console.log(this.age);
};
function Person (name, age) {
Mother.call(this, age);
this.name = name;
}
inheritPrototype(Person, Mother);
Person.prototype.showName = function () {
console.log(this.name);
}
var p1 = new Person('Jack', 20);
p1.hobby.push('basketball');//p1:'Jack'; __proto__:20,['running','football']
var p2 = new Person('Mark', 18); //p2:'Mark'; __proto__:18,['running','football'] 其运行结果如下所示:


此时原型中不再包含 age 和 hobby 属性,仅保留两个方法,这正是我们期望的结果。
关键点在于 object(o) 函数内部。这里借助一个临时对象巧妙避免了直接调用 new Mother(),并将原型为 o 的新对象实例返回,从而完成了原型链的设置。这一过程确实较为复杂,主要原因在于我们不能直接设置 Person.prototype = Mother.prototype。
小结
-------------------------------------------------------------------------------
JavaScript 对象创建模式总结
综上所述,核心要点只有一个:对属性共享和独立的控制。当对象实例需要独立属性时,所有做法的本质都是在对象实例内部创建属性。若不考虑过多细节,完全可以在 Person 构造函数中直接定义所需独立属性,以覆盖原型中的同名属性。总之,在使用原型继承时,需特别注意原型中的属性,因为它们具有牵一发而动全身的影响。
下面简要列举 JavaScript 中创建对象的几种常见方法。目前最常用的方法是组合模式,熟悉该模式的读者可直接跳至文章末尾点赞。
第一种方法:原始模式。
//1.原始模式,对象字面量方式
var person = {
name: 'Jack',
age: 18,
sayName: function () { alert(this.name); }
};
//1.原始模式,Object构造函数方式
var person = new Object();
person.name = 'Jack';
person.age = 18;
person.sayName = function () {
alert(this.name);
}; 显然,当我们需要批量创建 person1、person2 等对象时,每次都要编写大量重复代码,即便是资深的“复制粘贴员”也会感到疲惫。于是,批量生产的工厂模式应运而生。
第二种方法:工厂模式。
//2.工厂模式,定义一个函数创建对象
function creatPerson (name, age) {
var temp = new Object();
person.name = name;
person.age = age;
person.sayName = function () {
alert(this.name);
};
return temp;
}工厂模式实现了对象的批量化生产。只需指定姓名和年龄,即可批量生成对象实例,极大解放了双手。然而,由于工厂模式内部运作不透明,无法识别对象的实际类型(instanceof 测试结果为 Object),导致人狗不分。此外,每次创建对象时都要生成一个独立的 temp 对象,导致代码臃肿,不够优雅。
第三种方法:构造函数模式。
//3.构造函数模式,为对象定义一个构造函数
function Person (name, age) {
this.name = name;
this.age = age;
this.sayName = function () {
alert(this.name);
};
}
var p1 = new Person('Jack', 18); //创建一个p1对象
Person('Jack', 18); //属性方法都给window对象,window.name='Jack',window.sayName()会输出Jack 构造函数与 C++、Java 中的类构造函数类似,易于理解。此外,Person 可作为类型标识(instanceof 测试结果为 Person 和 Object)。但是,所有实例依然是独立的,不同实例的方法实际上是不同的函数引用。此处可忽略“函数”一词,将 sayName 视为一个对象即可理解:张三的 sayName 和李四的 sayName 是不同的存在。然而,我们通常期望共用一个 sayName 以节省内存。
第四种方法:原型模式。
//4.原型模式,直接定义prototype属性
function Person () {}
Person.prototype.name = 'Jack';
Person.prototype.age = 18;
Person.prototype.sayName = function () { alert(this.name); };
//4.原型模式,字面量定义方式
function Person () {}
Person.prototype = {
name: 'Jack',
age: 18,
sayName: function () { alert(this.name); }
};
var p1 = new Person(); //name='Jack'
var p2 = new Person(); //name='Jack' 需要注意的是原型属性和方法的共享特性,即所有实例中引用的都是原型中的属性和方法。任何一处对原型属性的修改都会引起其他实例的变化。
第五种方法:混合模式(构造函数+原型)。
//5. 原型构造组合模式,
function Person (name, age) {
this.name = name;
this.age = age;
}
Person.prototype = {
hobby: ['running','football'];
sayName: function () { alert(this.name); },
sayAge: function () { alert(this.age); }
};
var p1 = new Person('Jack', 20);
//p1:'Jack',20; __proto__: ['running','football'],sayName,sayAge
var p2 = new Person('Mark', 18);
//p1:'Mark',18;__proto__: ['running','football'],sayName,sayAge 该做法将需要独立的属性和方法放入构造函数中,而可以共享的部分则放入原型中。这样做可以在最大限度节省内存的同时,保留对象实例的独立性。