Skip to content

原型链是什么

为了直观地说明原型链是什么,先看一段代码:

js
/**
 * 一个普通函数,最为常用
 */
function sayHi() {
  console.log(`你好,我是${this?.name}`)
}

/**
 * 构造函数
 * 从技术上来说和普通函数并无区别,但有几个特点:
 * 1. 首字母大写
 * 2. 内部使用了 this 这个变量
 * 3. 使用 new 操作符调用
 *
 * 上述的几点特征只是约定俗成,并没什么强制性,不是说只有构造函数才有这三点特征
 * 实际上上面的普通函数 sayHi 里也用了 this
 */
function Person(name) {
  this.name = name
}

/**
 * 在Person构造函数的原型上添加一个sayHi普通函数
 */
Person.prototype.sayHi = sayHi

/**
 * 另一个构造函数
 */
function Animal(name) {
  this.name = name
}

/**
 * 构造函数使用 new 来调用,new 这个操作符的内部机制后面会说,总而言之,它会返回一个新的对象
 */
const 张三 = new Person('张三')
const 李四 = new Person('李四')
const 熊猫 = new Animal('熊猫')

/**
 * 普通函数也能 new 出来一个对象,但没什么意义,因为内部没有在 this 上附加属性
 */
const 莫名其妙 = new sayHi()

console.log('> 构造函数: Person')
console.dir(Person)

console.log('> 函数: sayHi')
console.dir(sayHi)

console.log('> Person实例: 张三')
console.dir(张三)

console.log('> Person实例: 李四')
console.dir(李四)

console.log('> sayHi实例: 莫名其妙')
console.dir(莫名其妙)

/**
 * 实例.__proto__实际上不是一个标准写法,只是因为多数JS引擎都实现了__proto__这个特殊属性所以保留了下来而已
 * 标准的写法应该是Object.getPrototypeOf(实例)
 */
console.log('> 张三.__proto__')
console.log(张三.__proto__)

console.log('> 李四.__proto__')
console.log(李四.__proto__)

/**
 * 实例.__proto__ === Object.getPrototypeOf(实例)
 */
console.log('> 李四.__proto__ === Object.getPrototypeOf(李四)')
console.log(李四.__proto__ === Object.getPrototypeOf(李四))

/**
 * 由于张三和李四都是Person的实例,因此两者的__proto__对象相同
 */
console.log('> 张三.__proto__ === 李四.__proto__')
console.log(张三.__proto__ === 李四.__proto__)

/**
 * 毋庸置疑,这两个肯定不相同,不然就乱套了
 */
console.log('> 张三.__proto__ === 熊猫.__proto__')
console.log(张三.__proto__ === 熊猫.__proto__)

/**
 * 注意,张三的__prpto__并不是Person这个构造函数,而是Person构造函数的prototype
 * 这里大家可能会突然疑惑,怎么又多出来一个prototype,到这里只需要记住,prototype是函数特有的属性
 * prototype的作用是作为构造函数 new 出来的所有实例的共同可访问对象,用来节省内存,也就是实例的__proto__
 */
console.log('> 张三.__proto__ === Person')
console.log(张三.__proto__ === Person)

/**
 * 再次强调一遍,实例的__proto__指向的是其构造函数的prototype,因此只要在构造函数的prototype上附加的属性
 * 这个构造函数的所有实例都可以访问到
 */
console.log('> 张三.__proto__ === Person.prototype')
console.log(张三.__proto__ === Person.prototype)

/**
 * 当然,你也可能好奇构造函数的__proto__是什么东西,首先,构造函数和函数在技术上并无区别,都可以认为是一个函数
 * 其次,在JS中函数也是一个对象,因此对象(这里是函数)的__proto__指向的是其构造函数的prototype也成立
 * 函数作为对象存在,它的构造函数其实是 Function,也就是说,函数的__proto__等于Function.prototype
 *
 */
console.log('> Person.__proto__')
console.log(Person.__proto__)

/**
 * 上面说过构造函数的prototype是给其所有实例共享的一个对象,因此,不同构造函数的prototype肯定是不同的,不然就乱套了
 */
console.log('> Animal.prototype === Person.prototype')
console.log(Animal.prototype === Person.prototype)

/**
 * 再次强调一遍,Object.getPrototypeOf(Person)是__proto__的标准写法
 * 构造函数.__proto__ === Function.prototype !== Person.prototype
 */
console.log('> Object.getPrototypeOf(Person) === Person.prototype')
console.log(Object.getPrototypeOf(Person) === Person.prototype)

/**
 * Object.getPrototypeOf(实例) === 实例.__proto__ === 实例的构造函数.prototype
 */
console.log('> Object.getPrototypeOf(张三) === Person.prototype')
console.log(Object.getPrototypeOf(张三) === Person.prototype)

/**
 * Object.getPrototypeOf(构造函数) === 构造函数.__proto__ === Function.prototype
 */
console.log('> Object.getPrototypeOf(Person) === Function.prototype')
console.log(Object.getPrototypeOf(Person) === Function.prototype)

/**
 * 一个很特殊的情况:Function.__proto__ === Function.prototype
 * 其实细想之下也可以理解,Function是一个对象,对象的__proto__指向的是其构造函数的prototype
 * 而恰好,Function作为对象,构造函数就是 Function,因此 Function.__proto__ === Function.prototype
 * 虽然两者相同,但意义不一样,实际上这个特例并没有特殊记忆的必要
 */
console.log('> Function.__proto__ === Function.prototype')
console.log(Function.__proto__ === Function.prototype)

/**
 * 原型对象也是个普通对象,它也有自己的原型对象,可以一直这么续下去直到 Object.prototype.__proto__,也就是 null
 */
console.log('> 张三.__proto__.__proto__ === Object.prototype')
console.log(张三.__proto__.__proto__ === Object.prototype)

/**
 * 孤立地调用普通函数时,其内部的 this 为undefined
 * 上面这句话实际上不是绝对的,在非 strict 模式下孤立函数的 this 指向的是 window对象
 * 但现在的多数环境下都是 strict 模式,所以直接认为孤立函数内的 this 是 undefined还要把稳些
 */
console.log('> sayHi()')
sayHi()

console.log('> 张三.sayHi()')
张三.sayHi()

console.log('> 李四.sayHi()')
李四.sayHi()

TIP

请打开控制台查看上述 JS 的实际输出

经过上述车轱辘话来来回回地反复说了又说,我们可以总结出以下三点,最起码得要能分清楚 __proto__prototypeObject.getPrototypeOf这几个名词才行,如果不能分清楚,那可能还需要看几遍上面的代码和其运行的结果。

  1. __proto__是野路子写法,因为多数 JS 环境都实现了这个属性,因此也经常被用到,但因为它不是语言标准,所以尽量避免使用。

proto不是标准

已弃用: 不再推荐使用该特性。虽然一些浏览器仍然支持它,但也许已从相关的 web 标准中移除,也许正准备移除或出于兼容性而保留。请尽量不要使用该特性,并更新现有的代码;参见本页面底部的兼容性表格以指导你作出决定。请注意,该特性随时可能无法正常工作。

proto的替代

备注: 使用 proto 是有争议且不被鼓励的。它的存在和确切行为仅作为遗留特性被标准化,以确保 Web 兼容性,但它存在一些安全问题和隐患。为了更好的支持,请优先使用 Object.getPrototypeOf()/Reflect.getPrototypeOf() 和 Object.setPrototypeOf()/Reflect.setPrototypeOf()。

  1. Object.getPrototypeOf()__proto__的标准写法,两者是一样的,后续的段落里一般只用__proto__

为何写博客时用非标准的__proto__

为什么不用标准的Object.getPrototypeOf(),大概是因为懒吧,而且__proto__比较适合写在等式中,但需要注意,正式环境下使用__proto__是应当被禁止的。

  1. prototype是函数特有的属性,普通对象是没有这个属性的,它的作用是共享实例的一部分内容以节省内存。

好了,上面又是车轱辘话说了一大堆,那我们不禁要问,说了这么多,那到底什么是他妈的原型链!

终于说出的原型链的定义

原型链是 JavaScript 中实现继承的一种机制,在 JavaScript 中,每个对象都有一个指向其原型对象的内部链接(__proto__),这个原型对象也是一个对象,它有自己的属性和方法,其中可能包括指向另一个原型的链接,如此层层递进,直到达到一个原型为 null 的对象为止,这样就形成了一条原型链。当一个对象访问某个属性时,如果该对象自身没有这个属性,JavaScript 就会沿着原型链向上查找,直到找到该属性或者达到原型链的顶端(null)为止。这种方式允许对象从它的原型及原型链上的其他对象那里继承属性和方法。

之所以上面要说那么多内容,就是为了原型链的定义,举例来说,张三这个对象上并没有sayHi方法,于是去张三.__proto__上,也就是去Person.prototype上找,而Person.prototype.sayHi = sayHi,因此可以调用到sayHi方法。

拿一个更通用的例子来说,我们经常会调用string.toUpperCase()这样的函数,字符串本身肯定是没这个方法的,因此会去String.prototype上找,这上面就有一大堆和字符串相关的方法,因此可以正确调用,如果有注意到 MDN 上的菜单,也可以看出这一点来:

alt text