学习原型链(一)创建对象

构造函数、原型对象与实例

抛开「构造」二字,只要创建了一个函数,就会有与其对应的原型对象。其关系如下:

/* 函数 */
function Person () { ... }

/* 原型对象 */
Person.prototype = {
  constructor: Person
}
复制代码

函数 Personprototype 属性指向其原型对象,原型对象 Person.prototypeconstructor 属性又反过来指向了函数。二者通过这种关系彼此关联起来。

回到构造函数上来。如果此时我们使用构造函数 Person 新建了一个实例 person1,实例与构造函数间是没有直接联系的,实例的 __proto__ 属性指向构造函数的原型对象 Person.prototype。因此,访问实例的构造函数,只能通过间接地在原型对象中拿到 constructor 属性。

var person1 = new Person();
console.log( person1.__proto__ === Person.prototype ); /* true */
console.log( person1.__proto__.constructor === Person ); /* true,此处仅仅是为了演示,实际上不必显式地写 __proto__ ,JS会自动地去原型链上寻找 constructor 属性 */
复制代码

创建对象

1. 工厂模式

除了使用字面量创建对象以外,工厂模式是最简单的创建对象的方法。

function createPerson (name, age) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.sayName = function () {
    console.log(this.name);
  };
  return o;
}
var person1 = createPeron('Nicholas', 29);
var person2 = createPeron('Grey', 27);
复制代码

使用工厂模式,每次总是新建并返回一个全新的对象。可以看出,person1person2 仅仅是有相同名字的属性和函数,但二者之间没有任何关联,与 createPerson 工厂函数不存在关系,更没有原型对象,因此我们无法识别其类型。

2. 构造函数模式

使用 new 操作符,可以将普通函数用作构造函数。实际上是将 Personnew 的空白对象作用域中执行。

function Person (name, age) {
  this.name = name;
  this.age = age;
  this.sayName = function () {
    console.log(this.name);
  };
}
var person1 = new Person('Nicholas', 29);
var person2 = new Person('Grey', 27);
复制代码

这里正是我们在第一节中描述的构造函数、原型对象和实例的关系。构造函数 Person 有一个原型对象 Person.prototype,两个实例 person1person2__proto__ 均指向了这个原型对象,实例通过原型对象访问到构造函数 Person,借助于这个关系可以解决新建对象的类型识别问题。

但在构造函数中的 sayName 方法,其实是每次 new 时新建的,两个函数是在内存中独立存在。

console.log( person1.sayName === person2.sayName ); /* false */
复制代码

由于其完成相同的功能,应将其视为一个「公共函数」。完成相同功能的「公共函数」,没必要在内存中存在两个副本。把 sayName 的定义转移到构造函数外可以解决这个问题。

function sayName () {
  console.log(this.name);
}
function Person (name, age) {
  this.name = name;
  this.age = age;
  this.sayName = sayName;
}
var person1 = new Person('Nicholas', 29);
var person2 = new Person('Grey', 27);
console.log( person1.sayName === person2.sayName ); /* true */
复制代码

公共函数 sayName 现在在内存中仅有一个副本了。但新的问题出现了,它被暴露在全局环境下,任何对象都可调用它。这不该是一个对象的内部函数该有的情况。继续改进,使用原型模式!

3. 原型模式

前面我们提到,每个构造函数都有一个原型对象,实例可以通过 __proto__ 访问原型对象。当访问实例的属性时,JS 会借助 __proto__ 自动地在原型链上一层层地向上寻找目标属性名对应的属性值。

由于构造函数在内存中只有一份,所以原型对象也只有一份。原型模式就是把属性和方法都定义在原型对象中。

function Person () {
}

Person.prototype.name = 'Nicholas';
Person.prototype.age = age;
Person.prototype.sayName = function () {
  console.log(this.name);
};

var person1 = new Person();
var person2 = new Person();
复制代码

基于原型链的特性,当我们访问实例 person1name 属性时,返回的是原型对象中的 name 属性。由于原型对象只有一份,所以使用构造函数 Person 创建的实例共享同一份属性和函数。

显然,我们希望每个实例有自己独立的属性。可以在实例中创建同名属性,在原型链的最前端「屏蔽」原型对象中的属性,这样创建的属性就是属于每个实例的了。

console.log(person1.name); /* Nicholas */
console.log(person2.name); /* Nicholas */

person1.name = 'Rob';

console.log(person1.name); /* Rob - 实例属性 */
console.log(person2.name); /* Nicholas - 原型对象属性 */

delete person1.name; /* delete 用于删除实例属性 */
console.log(person1.name); /* Nicholas - 原型对象属性 */
console.log(person2.name); /* Nicholas - 原型对象属性 */
复制代码

这种「添加同名属性」来「隐藏原型属性」的方法对于基本类型来讲勉强可以使用。然而,在这个问题上,给引用类型带来的麻烦更为突出。

function Person () {
}

Person.prototype.name = 'Nicholas';
Person.prototype.friends = ['Shelby', 'Court'];
};

var person1 = new Person();
var person2 = new Person();

person1.friends.push('Van');
console.log(person1.friends); /* 'Shelby, Court, Van' */
console.log(person2.friends); /* 'Shelby, Court, Van' */
复制代码

我们使用原型模式的初衷是,让需要共享的方法在实例间共享。令人苦恼的是,不需要共享的属性也被共享了。有没有一种方法可以自由地决定属性的共享/不共享呢?

4. 组合使用构造函数模式与原型模式

我们在使用构造函数时,内部函数 sayName 是非共享的,每个实例拥有一份 sayName 函数的副本。准确的讲,构造函数模式下所有的属性和方法都是在实例中定义的,因此是非共享的。我们利用这一点来改造原型模式。

核心思想是,将非共享属性放在构造函数中定义,最终这些非共享属性将各自属于自己的实例;将方法和共享属性放在原型对象中,所有实例共同使用一份副本。

function Person (name, age) {
  this.name = name;
  this.age = age;
  this.friends = ['Shelby', 'Court'];
}

Person.prototype.sayName = function () {
  console.log(this.name);
};

var person1 = new Person('Nicholas', 29);
var person2 = new Person('Greg', 27);
复制代码

这是目前 ECMAScript 中使用最广泛、认同度最高的一种创建自定义类型的方法。

原型模式的另一种简单写法

上面我们介绍的原型模式,都是在 Person.prototype 直接添加属性/方法,写法是这样的:

Person.prototype.name = 'Nicholas';
Person.prototype.age = 27;
Person.prototype.sayName = function () {
  console.log(this.name);
};
复制代码

如果我们使用字面量的方式重写,代码会简介得多:

Person.prototype = {
  name: 'Nicholas',
  age: 27,
  sayName: function () {
    console.log(this.name);
  };
}
复制代码

但回想第一节介绍的知识,原型对象 Person.prototype 的属性 constructor 指向了构造函数 Person,实例 person1__proto__ 属性指向了原型对象 Person.prototype ,这是从实例寻找构造函数的唯一途径。当我们用字面量的方式重写原型对象时,原型对象中的 constructor 会指向字面量创建对象的构造函数 Object。这是我们不希望看到的,我们再手动把 constructor 的指向修正为 Person

Person.prototype = {
  constructor: Person,
  name: 'Nicholas',
  age: 27,
  sayName: function () {
    console.log(this.name);
  };
}
复制代码

按照这种方式使用原型模式就比较稳妥了。

欢迎大家指正、补充!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值