1.2.1、理解原型设计模式以及JavaScript中的原型规则(JavaScript基础-原型和原型链)

400 阅读4分钟

原型设计模式

原型设计模式就是在通过构造函数创建对象实例时,通过共享原型对象的属性和方法,从而达到提高性能、降低内存占用、代码复用的目的。

首先要明白三个概念

  1. 构造函数
  2. 原型对象
  3. 实例对象

1、构造函数

1)、面向对象编程(Object Oriented Programming,缩写为OOP)是目前主流的编程范式。它将真实世界各种复杂的关系,抽象为一个对戏那个,然后由对象之间的分工与合作,完成对真实世界的模拟。

2)、面向对象编程第一步就是要生成对象。对象是单个实物的抽象。通常需要一个模版,表示某一类实物的共同特征,然后对象根据这个模版生成。

3)、JavaScript使用构造函数(constructor)作为对象的模版。所谓"构造函数",就是专门用来生成实例对象的函数。它就是对象的模版,描述实例对象的基本结构。

4)、一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。都可以使用这个构造函数的属性和方法。

构造函数实际上就是一个普通的函数,不过函数名一般以大写字母开头,表明这是一个构造函数。

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

以上代码即声明了一个Person的构造函数,有一个name属性和一个sayName方法。this实际指向了由此构造函数生成的实例对象。

2、原型对象

MDN表述:JavaScript常被描述为一种基于原型的语言(prototype-based language)——每个对象拥有一个原型对象,对象以其原型为模版、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链(prototype chain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。准确的说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非对象实例本身。

我们每创建一个函数,无论是构造函数还是普通函数(本质上是一样的),函数内部都会有一个特殊的属性--prototype,这个属性指向它的原型对象。

function Person() {}
console.log(Person.prototype)

控制台输入如下:

{
    constructor: ƒ Person()
    [[Prototype]]: Object
}

可以看到Person函数具有一个属性prototype,指向的是一个对象(原型对象),而这个对象有一个constructor属性,指向的就是Person函数。 而通过这个函数构造出来的实例对象身上也有一个属性__proto__,指向的也是原型对象。

let p = new Person()
console.log(p.__proto__)

输出如下

{
    constructor: ƒ Person()
    [[Prototype]]: Object
}

3、实例对象

实例对象就是使用new操作符正常调用函数时,它就会返回一个这个函数的实例化对象,然后就可以在这个对戏那个上面添加一些属性。其实上面已经通过Person构造函数生成了一个实例对象p。


ok,三个概念已经理解,就可以看看原型设计模式了 😏

首先,搞一个构造函数

function Person(name) {
    this.name = name
    this.sayName = function() {
        console.log(this.name)
    }
}
const p1 = new Person('yiyiyi')
p1.sayName() // yiyiyi
const p2 = new Person('qiqiqi')
p2.sayName() // qiqiqi

可以看到上面定义了一个名为Person的构造函数,并通过这个构造函数生成了两个实例p1、p2,但问题是如果生成了100个实例,那岂不是要生成100个sayName方法😨,这也太恐怖了,所以通过原型设计模式可以这么搞:

function Person(name) {
    this.name = name
}
Person.prototype = {
    sayName: function() {
        console.log(this.name)
    }
}
const p1 = new Person('yiyiyi')
p1.sayName() // yiyiyi
const p2 = new Person('qiqiqi')
p2.sayName() // qiqiqi

这样不就好了嘛🎉,把通用的方法定义在构造函数的原型对象上,而通过此构造函数生成的实例对象调用sayName方法的时候,先在自己本身寻找sayName方法,如果找不到则通过__proto__属性寻找它的原型对象上有没有这个方法,找不到则继续向上找(这就是原型链)。

有一个问题,向构造函数的原型对象写属性和方法要在生成实例对象之前,如果先生成实例对象,再向原型对象写这个属性和方法,之前生成的实例就会找不到。

function Person(name) {
    this.name = name
}
const p3 = new Person('yiyiyi')
Person.prototype = {
    sayName: function() {
        console.log(this.name)
    }
}
const p4 = new Person('qiqiqi')
p3.sayName() // 报错 p1.sayName is not a function
p4.sayName() // qiqiqi

其实也好理解,生成p1,p1的__proto__指向原型对象,众所周知,引用类型数据存的是地址,然后又通过上面的方式给Person的原型对象写方法,相当于指向了另一个对象,但是p1的__proto__指向没变,所以会报错,没毛病。