JavaScript原型与封装机制详解:从构造函数到Object.create

作者:闭包旅人

时间:2026-10-30

浏览:540

本文详细解析JavaScript中的原型继承机制,探讨构造函数在资源浪费上的缺陷,并深入讲解prototype对象的作用、原型链的查找规则以及constructor属性的意义。同时介绍Object.create方法如何灵活创建继承对象,以及如何利用isPrototypeOf判断原型关系,最后简述对象封装的实现思路。

文章目录

构造函数的局限性

1. prototype对象 1.1 构造函数的局限性 在JavaScript中,开发者通常利用构造函数来创建新对象,此时构造函数可被视为对象的模板。实例对象的属性和方法可以直接定义在构造函数内部。


function Cat (name, color) {
 this.name = name;
 this.color = color;
}

var cat1 = new Cat('大毛', '白色');

cat1.name // '大毛'
cat1.color // '白色'

上述代码中的Cat函数是一个典型的构造函数,它在函数体内定义了name和color两个属性,这意味着所有通过该构造函数创建的实例对象都会拥有这两个属性。然而,这种实现方式存在系统资源浪费的问题,因为同一构造函数的不同实例对象之间无法共享这些属性。


function Cat(name, color) {
 this.name = name;
 this.color = color;
 this.meow = function () {
 console.log('mew, mew, mew...');
 };
}

var cat1 = new Cat('大毛', '白色');
var cat2 = new Cat('二毛', '黑色');

cat1.meow === cat2.meow
// false

在上面的代码示例中,cat1和cat2虽然是由同一个构造函数生成的实例,但它们各自的meow方法却是不同的对象实例。这意味着每次创建新实例时,都会重新生成一个meow方法。这种做法不仅没有必要,还消耗了系统资源,因为所有meow方法的行为完全一致,理应被共享。

prototype属性的核心作用

1.2 prototype属性的核心作用 在JavaScript语言体系中,每个对象都关联着一个对应的原型对象,即prototype对象。定义在该原型对象上的所有属性和方法,都可以被派生出的对象继承。这正是JavaScript继承机制的基础设计理念。

除了直接在构造函数内部定义属性外,JavaScript还提供了另一种定义实例对象属性的方式。构造函数本身既是函数也是对象,拥有自己的属性和方法,其中包含一个prototype属性,该属性指向另一个对象,通常被称为prototype对象。这个对象具有特殊性:定义在其上的属性和方法可以被所有实例对象共享。也就是说,当通过构造函数生成实例对象时,系统会自动为实例对象分配一个指向该prototype对象的引用。


function Animal (name) {
 this.name = name;
}

Animal.prototype.color = "white";

var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');

cat1.color // 'white'
cat2.color // 'white'

上述代码展示了向构造函数Animal的prototype对象添加color属性的过程。结果显示,实例对象cat1和cat2都成功拥有了该属性。

更为特殊的是,一旦修改prototype对象,这种变动会立即反映在所有现有的实例对象上。


Animal.prototype.color = "yellow";

cat1.color // 'yellow'
cat2.color // 'yellow'

在上述代码中,将prototype对象的color属性值修改为yellow后,两个实例对象cat1和cat2的color属性值也随之立即变为yellow。这是因为实例对象本身并没有color属性,它们实际上是读取prototype对象上的color属性。换句话说,当实例对象自身不存在某个属性或方法时,它会去构造函数的prototype对象中查找该属性或方法。这就是prototype对象的独特之处。

如果实例对象自身已经定义了某个属性或方法,它将不再去prototype对象中查找该属性或方法。


cat1.color = 'black';

cat2.color // 'yellow'
Animal.prototype.color // "yellow";

在上述代码中,将实例对象cat1的color属性修改为black,这使得它不再需要从prototype对象读取color属性,而prototype对象中的color属性值依然保持为yellow。

综上所述,prototype对象的主要作用是定义所有实例对象共享的属性和方法,因此它也被称为实例对象的原型,而实例对象可以被视为从prototype对象衍生而来。


Animal.prototype.walk = function () {
 console.log(this.name + ' is walking.');
};

上述代码在Animal.prototype对象上定义了一个walk方法,该方法可以在所有Animal实例对象上被调用。

原型链(Prototype Chain)

1.3 原型链(Prototype Chain) 由于JavaScript中的所有对象都有构造函数,而所有构造函数都拥有prototype属性(实际上是所有函数都有prototype属性),因此所有对象都有自己的原型对象。

因此,一个对象的属性和方法可能定义在其自身上,也可能定义在其原型对象上(如上述代码中的walk方法)。由于原型本身也是对象,并且拥有自己的原型,从而形成了一条原型链(prototype chain)。例如,如果a对象是b对象的原型,b对象是c对象的原型,依此类推。由于追溯根源,所有对象最终都是由Object构造函数生成的(使用new Object()命令),因此如果层层上溯,所有对象的原型最终都可以追溯到Object.prototype。那么,Object.prototype是否有原型呢?答案是有,也可以是没有,因为Object.prototype的原型是没有任何属性和方法的null。


Object.getPrototypeOf(Object.prototype)
// null

上述代码表明Object.prototype对象的原型是null,由于null没有任何属性,因此原型链在此终止。

“原型链”的作用在于,当读取对象的某个属性时,JavaScript引擎首先寻找对象本身的属性,如果找不到,就去它的原型中查找,如果仍然找不到,就去原型的原型中查找。以此类推,如果直到最顶层的Object.prototype仍未找到,则返回undefined。

举例来说,如果让某个函数的prototype属性指向一个数组,这意味着该函数可以用作数组的构造函数,因为它生成的实例对象都可以通过prototype属性调用数组方法。


function MyArray (){}

MyArray.prototype = new Array();
MyArray.prototype.constructor = MyArray;

var mine = new MyArray();
mine.push(1, 2, 3);

mine.length // 3
mine instanceof Array // true

上述代码中的mine是MyArray的实例对象,由于MyArray的prototype属性指向一个数组,使得mine可以调用数组方法(这些方法实际上定义在数组的prototype对象上)。至于最后一行instanceof表达式,我们知道instanceof运算符用于判断一个对象是否为某个构造函数的实例,最后一行表示mine是Array的实例。


mine instanceof Array

// 等同于

(Array === MyArray.prototype.constructor) ||
(Array === Array.prototype.constructor) ||
(Array === Object.prototype.constructor )

上述代码揭示了instanceof运算符的实质,它依次与实例对象的所有原型对象的constructor属性(关于该属性的介绍,请看下一节)进行比较,只要有一个符合就返回true,否则返回false。

constructor属性

1.4 constructor属性 prototype对象拥有一个constructor属性,默认指向prototype对象所在的构造函数。


function P() {}

P.prototype.constructor === P
// true

由于constructor属性定义在prototype对象上,这意味着它可以被所有实例对象继承。


function P() {}

var p = new P();

p.constructor
// function P() {}

p.constructor === P.prototype.constructor
// true

p.hasOwnProperty('constructor')
// false

上述代码表示p是构造函数P的实例对象,但是p自身没有constructor属性,该属性实际上是读取原型链上的P.prototype.constructor属性。

constructor属性的作用是辨别prototype对象到底定义在哪个构造函数上。


function F(){};

var f = new F();

f.constructor === F // true
f.constructor === RegExp // false

上述代码表示,使用constructor属性,可以确定变量f的构造函数是F,而不是RegExp。

Object.getPrototypeOf方法

2. Object.getPrototypeOf方法 Object.getPrototypeOf方法用于返回一个对象的原型。


// 空对象的原型是Object.prototype
Object.getPrototypeOf({}) === Object.prototype
// true

// 函数的原型是Function.prototype
function f() {}
Object.getPrototypeOf(f) === Function.prototype
// true

// 假定F为构造函数,f为F的实例对象
// 那么,f的原型是F.prototype
var f = new F();
Object.getPrototypeOf(f) === F.prototype
// true

Object.create方法

3. Object.create方法 Object.create方法用于生成新的对象,可以替代new命令。它接受一个对象作为参数,返回一个新对象,后者完全继承前者的属性,即前者成为后者的原型。


var o1 = { p: 1 };
var o2 = Object.create(o1);

o2.p // 1

上述代码中,Object.create方法在o1的基础上生成了o2。此时,o1成为o2的原型,也就是说,o2继承了o1所有的属性和方法。

Object.create方法基本等同于以下代码,如果老式浏览器不支持Object.create方法,可以使用以下代码自行部署。


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

上述代码表示,Object.create方法的实质是新建一个构造函数F,然后让F的prototype属性指向作为原型的对象o,最后返回一个F的实例,从而实现让实例继承o的属性。

以下三种方式生成的新对象是等价的。


var o1 = Object.create({});
var o2 = Object.create(Object.prototype);
var o3 = new Object();

如果想要生成一个不继承任何属性(例如toString和valueOf方法)的对象,可以将Object.create的参数设为null。


var o = Object.create(null);

o.valueOf()
// TypeError: Object [object Object] has no method 'valueOf'

上述代码表示,如果对象o的原型是null,它就不具备一些定义在Object.prototype对象上的属性,例如valueOf方法。

使用Object.create方法时,必须提供对象原型,否则会报错。


Object.create()
// TypeError: Object prototype may only be an Object or null

Object.create方法生成的新对象,动态继承了原型。在原型上添加或修改任何方法,会立刻反映在新对象上。


var o1 = { p: 1 };
var o2 = Object.create(o1);

o1.p = 2;
o2.p
// 2

上述代码表示,修改对象原型会影响到新生成的对象。

除了对象的原型,Object.create方法还可以接受第二个参数,表示描述属性的attributes对象,其格式与Object.defineProperties方法相同。它所描述的对象属性,会添加到新对象中。


var o = Object.create(Object.prototype, {
 p1: { value: 123, enumerable: true },
 p2: { value: "abc", enumerable: true }
});

o.p1 // 123
o.p2 // "abc"

由于Object.create方法不使用构造函数,因此不能用instanceof运算符判断对象是哪一个构造函数的实例。这时,可以使用以下isPrototypeOf方法,判断原型是哪一个对象。

isPrototypeOf方法

4. isPrototypeOf方法 isPrototypeOf方法用于判断一个对象是否是另一个对象的原型。


var o1 = {};
var o2 = Object.create(o1);
var o3 = Object.create(o2);

o2.isPrototypeOf(o3) // true
o1.isPrototypeOf(o3) // true

上述代码表明,只要某个对象处于原型链上,isPrototypeOf都返回true。

对象封装示例

5. 一个简单示例


var ClassDemo = function () {

 //静态private变量
 var private_static_var = 'aaaa';

 //静态private方法
 var private_static_func = function (key) { 
  return key + private_static_var;
 }

 //private方法,关键就是第一参数self要传入this
 var private_func = function (self, key) { 
  return private_static_func(key + self.id);
 }

 var _class = function (id) { //构造函数
  this.id = id; //public变量
 }

 //public方法
 _class.prototype.public_func = function (key) { 
  return private_func(this, key);
 }

 return _class;

}();

var a = new ClassDemo('hello world');
alert(a.public_func('world hello'));

没有简单的办法实现private变量和public静态变量/方法,不过封装到这个程度已经足够使用。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章