Skip to content

理解原型链

JavaScript在设计之初,作为一种网页脚本语言,没有设计得很复杂,这种语言只要能够完成一些简单操作就够了。Javascript里面所有的数据类型都是对象(object)。在ES6之前,js中是没有Class的概念的(ES6中的类也是语法糖,本质还是基于原型),为了实现实例对象的属性和方法共享,就给function设计了一个prototype的概念。每个对象都有一个原型属性,原型属性指向另一个对象,而原型属性中的对象是会被其他对象所继承的,也就是共用的,对象自身的有些属性和方法则不可以。原型对象(prototype)也有一个自己的原型对象(__proto__),普通属性并非为对象

实例对象的隐式原型指向其构造函数的显示原型

prototype和__proto__

在传统的 OOP 中,首先定义“类”,此后创建对象实例时,类中定义的所有属性和方法都被复制到实例中。在 JavaScript 中并不如此复制——而是在对象实例和它的构造器之间建立一个链接(它是__proto__属性,是从构造函数的prototype属性派生的),之后通过上溯原型链,在构造器中找到这些属性和方法。

  • prototype:

    1. 函数的一个属性
    2. 是个对象
    3. 创建函数时会默认添加propertype这个属性
  • __proto__:

    1. 对象的一个属性:
      • Object.prototype的__proto__属性是一个访问器属性(一个getter函数和一个setter函数)
    2. 指向构造函数prototype
      • 公开访问它的对象的内部[[Prototype]](对象或null)
js
function ptt() {
    this.a = 1
}
const obj = new ptt()

// 函数:
console.dir(ptt)
console.log(ptt.prototype)
// 既然prototype是个对象,那么就有__proto__
console.log(ptt.prototype.__proto__)
console.log(ptt.prototype.__proto__ === Object.prototype)// true

// 对象:
console.dir(obj)
// 既然是对象,那么就有__proto__
console.log(obj.__proto__)
console.log(obj.__proto__ === ptt.prototype)//true
console.log(Object.prototype.__proto__)//null
ptt.prototype.b = 2

Object.prototype.c = 3

console.log(ptt.prototype.b)// 2
console.log(obj.c)// 3

以上代码生成的原型链结构如下:

js
obj {
  a:1
  __proto__:ptt.prototype = {
    b:2,
    __proto__:Object.prototype = f{
      c:3
      __proto__:null
    }
  }
}

在Chrome浏览器的开发者工具(F12)打印console.dir(obj)后如图:

实际上,Chrome 控制台打印的 [[Prototype]] 即表示对象的原型,并不需要一层层的去寻找__proto__:

难点在于……构造函数也是对象!

如果没有 prototype,那么共用属性就没有立足之地,

prototype 指向一块内存,这个内存里面有共用属性;

__proto__ 指向同一块内存;

如果没有__proto__,那么一个对象就不知道自己的共用属性有哪些。上面所说__proto__属性是一个访问器属性,通俗来说,prototype就是用__proto__来查找有哪些对象是可继承,且继承了的。

  1. prototype存储共用的属性和方法,
  2. __proto__用来将对象与该对象的原型相连,
  3. constuctor是用来将原型对象指向关联的构造函数。

原型链

通过原型链从而找到toString()方法,而其他对象同样具有此方法,所以并不是对象会把这些方法和属性复制过来,只是通过原型(prototype)找到被继承的对象的原型。

当我们创建一个对象后,就可以通过“点”方法名的方式调用一些并不是我们写的方法了,例如:

js
let num=123
num.toString()
console.log(num.toString === Number.prototype.toString); // true
console.log(Object.getPrototypeOf(num) === Number.prototype); // 输出: true

其实我们调用的是Number.prototype.toString。现在是不是对JavaScript是基于原型的语言这句 话有些理解了。

其实不止Number是这样的,Array、String、Object等都是这样的原理。

“JavaScript-一切皆对象”

构造函数的prototype属性与通过该构造函数创建的对象实例的原型是同一个对象,因此Object.getPrototypeOf(new Foobar()) === Foobar.prototype

原型的实践

create()

可以使用Object.create()的方法,创建原型对象,例如:

js
let person = {
  name: 'Rarrot'
};
var person1 = Object.create(person);

console.log(person1.__proto__);

创建了一个新的对象 person1,它的原型被设置为之前定义的 person 对象。通过 Object.create(person),person1 继承了 person 的所有属性和方法。

注意,虽然使用 __proto__ 属性来直接访问一个对象的原型在很多情况下都能工作,但这并不是推荐的做法,因为它并不是所有JavaScript环境都支持的标准特性。更标准的方式是使用 Object.getPrototypeOf() 方法来获取一个对象的原型。例如:

js
console.log(Object.getPrototypeOf(person1));// { name: 'Rarrot' }

constructor属性

每个实例对象都从原型中继承了一个constructor属性,该属性指向了用于该构造此实例对象的构造函数,例如:

js
//如果构造此对象的是person函数,返回person(),没有的话就返回Object()
console.log(person1.constructor); // ƒ Object() { [native code] }

如果想要创建一个新的实例,但是拿不到实例的引用,可以使用这个属性来找到构造函数,并使用 new 关键字来创建一个新的实例:

js
var person3 = new person1.constructor('Rarrot2');

修改原型

通过在构造函数的 prototype 上定义方法,可以实现一个强大的继承机制,其中通过这个构造函数创建的所有对象实例都可以访问这些方法。例如,假设有一个构造函数 Person,可以为它的原型添加一个 joke 方法:

js
function Person(firstName) {
    this.name = { first: firstName };
}

Person.prototype.joke = function () {
    alert(this.name.first + ' ahhh!');
};

var person = new Person('Rarrot');
person.joke(); // 弹出信息 "Rarrot ahhh!"

在这个例子中,任何由 Person 构造函数创建的实例(如 person3)都可以调用 joke 方法。

证明了先前的原型链模型这种继承模型下,上游对象的方法不会复制到下游的对象实例中;下游对象本身虽然没有定义这些方法,但浏览器会通过上溯原型链、从上游对象中找到它们。这种继承模型提供了一个强大而可扩展的功能系统。

一种常见的定义模型

在构造器(函数体)中定义属性、在 prototype 属性上定义方法。如此,构造器只包含属性定义,而方法则分装在不同的代码块,代码更具可读性。

js
// 构造器及其属性定义
function Test(a,b,c,d) {
  // 属性定义
};
// 定义第一个方法
Test.prototype.x = function () { ... }

// 定义第二个方法
Test.prototype.y = function () { ... }

典型的例子:school plan app

力扣

可以去力扣上找设计题目类型的题来理解原型链,例如:

384.打乱数组

155.最小栈

参考链接:

理解JS的prototype

小满zs 原型链

Released under the MIT License.