原型与原型链

531 阅读5分钟

首先我们应该会问为什么会有原型这个概念,其次原型是解决什么问题的。

提到原型,必须要提到构造函数。

构造函数

  • 构造函数是把客观事物封装成抽象的类,隐藏属性和方法,仅对外公开接口
  • 说白了构造函数就是包含一系列属性和方法的类,便于被其他实例或类继承,达到共享的效果。

私有属性、公有属性、静态属性

  • 私有属性和方法:只能在构造函数内访问不能被外部所访问(在构造函数内使用var声明的属性)
  • 公有属性和方法(或实例方法):对象外可以访问到对象内的属性和方法(在构造函数内使用this设置,或者设置在构造函数原型对象上)
  • 静态属性和方法:定义在构造函数上的方法(比如Cat.xxx),不需要实例就可以调用(例如Object.assign())
function Cat(name, color) {
    var heart = '❤️' //私有属性
    var stomach = '胃'  //私有属性
    var heartbeat = function () {
        console.log(heart + '跳')
    }   //私有方法this.name = name //公有属性
    this.color = color //公有属性this.jump = function () {
        heartbeat() // 能跳起来表明这只猫是活的,心也就能跳
        console.log('我跳起来了~来追我啊')
    } //公有方法
}
​
Cat.descript = '我这个构造函数是用来生产出一只猫的' //静态属性
Cat.actingCute = function () {
    console.log('一听到猫我就想到了它会卖萌')
} //静态方法Cat.prototype.cleanTheBody = function () {
    console.log('我会用唾液清洁身体')
} //公有方法

在构造函数中,由于公有属性和方法可以被实例共享,公有属性和方法也是用的最多的。但是公有属性和方法既可以在构造函数内部用this来定义,又可以设置在构造函数的原型上面,那是不是多此一举,直接定义在构造函数内部不好吗,为什么还要搞出原型的概念呢!

答案是肯定的,在构造函数内部用this定义属性和方法肯定有不足的地方,那不足的地方在哪呢?我们通过一个例子来解析。

function Cat(name, color) {
    this.name = name
    this.color = color
    this.num = 10
    
    this.jump = function () {
        console.log('我跳起来了~来追我啊')
    }
}
​
let cat1 = new Cat('mimi', 'red')
let cat2 = new Cat('xiaohei', 'black')
​
console.log(cat1.num); //10
console.log(cat2.num);  //10
console.log(cat1.num === cat2.num); //true
​
cat1.jump()  //我跳起来了~来追我啊
cat2.jump()  //我跳起来了~来追我啊
console.log(cat1.jump === cat2.jump); //false

通过上面的示例我们可以看出,对于公有属性而言,无论被多少实例继承都是一样的内容,但对于在构造函数内部用this定义的引用数据类型,每次生成实例,都是新开辟一个内存空间存方法。这样会导致内存的极大浪费,从而影响性能。

我们有没有什么方法可以使得每个实例共享同一种方法呢?

答案是有的,就是利用原型,在构造函数的原型上面设置方法,从而达到共享的作用。

function Cat(name, color) {
    this.name = name
    this.color = color
}
​
Cat.prototype.jump = function () {
    console.log('我跳起来了~来追我啊')
}
​
let cat1 = new Cat('mimi', 'red')
let cat2 = new Cat('xiaohei', 'black')
​
cat1.jump()  //我跳起来了~来追我啊
cat2.jump()  //我跳起来了~来追我啊
console.log(cat1.jump === cat2.jump); //true

除此之外,我们还可以对原型的方法进行修改,可以达到全部更新的效果。

Cat.prototype.jump = function () {
    console.log('我跳起来了')
}
​
cat1.jump()  //我跳起来了
cat2.jump()  //我跳起来了
console.log(cat1.jump === cat2.jump); //true

总结

原型是为了解决在构造函数内部用this定义公有方法时,每次生成实例,都是新开辟一个内存空间存方法。这样会导致内存的极大浪费,从而影响性能的问题。而原型可以达到共享的效果。

公共属性定义到构造函数里面,公共方法我们放到原型对象身上。

原型

概念

在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。

概念很多,我们就拆分下来解析一下。先看第一句话。

  • 在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。

上面这句话其实很好理解,通过上一章的构造函数,我们知道了构造函数为什么会有原型。上面这句活概括起来就是构造函数内部有一个prototype属性,这个属性包含了可以由所有实例共享的属性和方法。

下面看第二句

  • 当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。

简单理解就是当构造函数实例化后,实例对象内部包含一个指针用来指向可以共享的构造函数上在prototype 上定义的方法,这个指针就是对象的原型。

其实就是:

obj.__proto__ === Star.prototype

其中Star是构造函数,obj是实例化对象,而__proto__就是对象的原型。

原型链

概念

  • 当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。

这个概念其实很好理解,就是在对象的原型上一直找下去,直到找到或者找到头为止。

img

参考 2020面试收获 - js原型及原型链