原型、原型链以及继承(一)

本文主要是对 【小红书】 中原型和继承进行了归纳总结。

原型模式

  • 创建的每一个函数都包含一个 prototype (原型)属性,这个属性是一个指针,指向 原型对象 (即平时所说的 “原型”);
  • 所有的原型对象都会自动获得一个 constructor (构造函数)属性,这个属性也是一个指针,指向一个包含 prototype 属性所在的函数;
  • 调用构造函数创建实例,每个实例都包含一个 [[prototype]]_proto_)属性(内部属性),这个属性还是一个指针,指向构造函数的原型对象。

解释一下 \_proto\_:在脚本中没有标准的方式访问 [[prototype]],但 Firefox、Safari 和 Chrome 在每个对象上都支持一个属性(\_proto\_),它就相当于是对象实例与构造函数的原型对象之间的一个连接。

(对照着下图进行理解)

原型对象的用途: 包含可以由特定类型的所有实例共享的属性和方法;
使用原型对象的好处: 让所有对象实例共享它所包含的属性和方法,不必在构造函数中定义对象实例的信息,而是将这些信息直接添加到原型对象中。

下面,直接上代码和图,说明一下各个对象之间的关系。

各个对象之间的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 创建函数
function Person(){
}
// 给原型对象添加属性和方法
Person.prototype.name = "Bob";
Person.prototype.age = 29;
Person.prototype.job = "student";
Person.prototype.sayName = function(){
alert(this.name);
};
// 创建对象实例,并调用方法
var person1 = new Person();
person1.sayName(); // "Bob"
var person2 = new Person();
person2.sayName(); // "Bob"
alert(person1.sayName == person2.sayName); // true

上面代码中,首先创建了一个名为 Person 的构造函数。由图可以看出,Person 函数有一个 prototype 属性,它是一个指针,指向该函数的原型对象(Person Prototype);紧接着,给原型对象添加属性和方法,这些属性和方法可以由对象实例共享;最后,使用构造函数来创建对象实例,并调用方法。这里创建了两个对象实例:person1person2,他们拥有相同的属性和方法,因此最后一句代码弹出的是 true

原型对象

对象实例与原型对象的关系

  • isPrototypeOf(): 判断对象实例的 [[prototype]] 内部属性是否指向原型对象;

    1
    alert(Person.prototype.isPrototypeOf(person1)); // true
  • Object.getPrototypeOf(): 该方法返回 [[prototype]] 的值;

    1
    alert(Object.getPrototypeOf(person1)); // Person.prototype

实例对象访问属性

每当代码读取某个实例对象的属性时,都会按一下步骤来执行:

  • 首先搜索实例对象本身是否具有给定名字的属性,如果有则返回该属性的值;
  • 如果没有,则在原型对象中进行搜索,找到了则返回该属性的值。

原型对象最初只包含 constructor 属性,该属性也是共享的,对象实例可以访问到。

虽然可以通过对象实例访问保存在原型对象中的值,但却不能通过对象实例重写原型对象中的值。(如果实例对象中有一个属性与原型对象中的属性同名,则根据访问顺序,原型对象中的同名属性会被屏蔽;可以通过 delete 删除实例属性,让原型对象中的同名属性重见天日。)

hasOwnProperty()

用来判断某个属性是来源于实例对象本身,还是来源于原型对象
(只要当该对象来源于实例对象本身的时候,该方法才会返回 true

1
2
3
4
5
6
7
8
9
10
11
function Person(){
}
Person.prototype.name = "Bob";
Person.prototype.age = 29;
var person1 = new Person();
person1.name = "Alice";
alert(person1.hasOwnProperty("name")); // true
alert(person1.hasOwnProperty("age")); // false

原型对象与 in 操作符

  • 单独使用
    如果通过实例对象能够访问到给定的属性(无论来源于哪里),都会返回 true

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function Person(){
    }
    Person.prototype.name = "Bob";
    Person.prototype.age = 29;
    var person1 = new Person();
    person1.name = "Alice";
    alert("name" in person1); // true
    alert("age" in person1); // true
  • for-in 循环中使用
    返回的是所有能够通过实例对象访问的、可枚举的属性(无论该属性来源于哪里)。

注意:如果某个属性在原型对象中是不可枚举的(即该属性的 [[Enumerable]] 被设置为 false),实例对象中有一个同名属性,则该实例属性还是会在 for-in 循环中返回的。(所有开发人员定义的属性都是可枚举的,只有在IE8及更早版本中例外

可替代方法:Object.keys()Object.getOwnPropertyNames()

原型语法的简化

1
2
3
4
5
6
7
8
9
function Person(){}
Person.prototype = {
name: "Bob",
age: 29,
job: "student",
sayName: function(){
alert(this.name);
}
}

上面代码中,不需要在每次添加属性和方法的时候都敲一遍 Person.prototype,而是将 Person.prototype 设置为等于一个以对象字面量形式创建的新对象。

注意:这里 constructor 属性不再指向 Person 构造函数,因为这里完全重写了 Person.prototype 对象。

原型的动态性

  • 可以随时为原型对象添加属性和方法,并且修改能够立即在所有对象实例中反应出来;
  • 如果重写整个原型对象,实例对象中的 [[prototype]] 内部属性指向的还是最初的那个原型对象,而不是这个新的原型对象;

原型对象的问题

  • 所有实例在默认情况下会取得相同的属性值,在某种程度上会带来一些不方便;
  • 由其共享的本性所导致 —— 特别是包含引用类型值的属性的时候
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function Person(){}
    Person.prototype = {
    constructor: Person,
    name: "Bob",
    age: 29,
    friends: ["Alice","Lucy"]
    }
    var person1 = new Person();
    var person2 = new Person();
    person1.friends.push("Cindy");
    alert(person1.friends); // "Alice, Lucy, Cindy"
    alert(person2.friends); // "Alice, Lucy, Cindy"
    alert(person1.friends == person2.friends); // true

上面代码中,Person.prototype 原型对象中的 friends 属性是一个数组,而 person1person2 这两个实例对象访问属性 friends 时,其实指向的是同一个数组,所以对 person1.friends 的修改也会在 person2.friends 上反映出来。

但是……
如果是在 person1 实例对象中重写 friends 属性就不一样了。然而如果想在原先属性的基础上做修改,这种方法还是解决不了问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person(){}
Person.prototype = {
constructor: Person,
name: "Bob",
age: 29,
friends: ["Alice","Lucy"]
}
var person1 = new Person();
var person2 = new Person();
person1.friends = ["Alice","Lucy","Cindy"];
alert(person1.friends); // "Alice, Lucy, Cindy"
alert(person2.friends); // "Alice, Lucy"

因此,基于以上问题,原型模式很少单独使用。

对原型模式的改进

组合使用构造函数模式和原型模式

—— 使用最广泛、认同度最高

  • 构造函数模式: 定义实例属性;
  • 原型模式: 定义方法和共享的属性;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 构造函数模式
function Person(name,age){
this.name = name;
this.age = age;
this.friends = ["Alice","Lucy"];
}
// 原型模式
Person.prototype = {
constructor: Person,
sayName: function(){
alert(this.name);
}
};
var person1 = new Person("Bob",29);
var person2 = new Person("Petter",28);
person1.friends.push("Cindy");
alert(person1.friends); // "Alice,Lucy,Cindy"
alert(person2.friends); // "Alice,Lucy"
alert(person1.friends == person2.friends); // false
alert(person1.sayName == person2.sayName); // true

动态原型模式

  • 把所有信息都封装在构造函数中,通过在构造函数中初始化原型对象(仅在必要的情况下),保持了同时使用构造函数和原型的优点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Person(name,age){
this.name = name;
this.age = age;
this.friends = ["Alice","Lucy"];
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
}
Person.job = "student";
}
var person1 = new Person("Bob", 29);
person1.sayName(); // Bob
alert(person1.job); // student

以上代码中,只有在 sayName() 方法不存在的情况下,才会将 sayName() 方法和 job 属性添加到原型中。if 语句部分,只会在初次调用构造函数时才执行, 之后原型对象已经初始化,不需要再做什么修改。
if 语句检查的可以是初始化之后应该存在的任何属性和方法,如果不止一个,也不需要每一个属性和方法都检查一遍,只要检查其中一个即可。(上面代码就只检查了一个)

寄生构造函数模式

稳妥构造函数模式

您的支持将鼓励我继续创作!