立诚勿怠,格物致知
It's all about connecting the dots

JS原型与原型链

一、普通对象与函数对象

JS中,对象分为普通对象和函数对象两种,Object和Function是JS自带的函数对象。凡是通过new Function()的创建的对象都是函数对象,其他的都是普通对象。

下面这些是函数对象:

typeof Object // 'function', 函数对象
typeof Function // 'function', 函数对象
typeof Date // 'function', 函数对象
typeof Promise // 'function', 函数对象
typeof Array // 'function', 函数对象
typeof RegExp // 'function', 函数对象

function f1 () {}
var f2 = function () {}
var f3 = new Function('str', 'console.log(str)')

typeof f1 // "function", 函数对象
typeof f2 // "function", 函数对象
typeof f3 // "function", 函数对象

下面这些是普通对象:

var o1 = new f1()
var o2 = {}
var o3 = new Object()

typeof o1 // "object", 普通对象
typeof o2 // "object", 普通对象
typeof o3 // "object", 普通对象

二、原型对象

每当定义一个对象(函数)时,对象中都会包含一些预定义的属性。其中,函数对象会有一个prototype属性,其值就是我们所说的原型对象(普通对象没有prototype,但有__proto__属性;函数对象同时含有prototype和__proto__属性)。注意__proto__这里proto前后分别都是两个下划线,不是一个。

原型对象其实就是普通对象(Function.prototype除外,它是函数对象,但同时它又没有prototype属性)。

function f1 () {}
console.log(f1.prototype) // 一个含constructor、__proto__等属性的对象
typeof f1.prototype // "object"

typeof Object.__proto__ // "function"

// 特例,没必要记住,平常根本用不到
typeof Function.prototype // "function"
typeof Function.prototype.prototype // "undefined"
typeof Object.prototype // "object"

原型对象的主要作用是用于继承:

var Person = function (name) {
  this.name = name
}

Person.prototype.getName = function () {
  return this.name
}

var yakima = new Person('yakima')
yakima.getName() // "yakima"

三、原型链

上面提到原型对象的主要作用是用于继承,其具体的实现就是通过原型链实现的。创建对象(不论是普通对象还是函数对象)时,都有一个叫做__proto__的内置属性,用于指向创建它的函数对象的原型对象(即创建它的函数对象的prototype属性)。

yakima.__proto__ === Person.prototype // true,对象的内置__proto__对象指向创建该对象的函数对象的prototype

Person.prototype.__proto__ === Object.prototype // true,Person.prototype这个原型对象是普通对象,是通过Object函数对象创建的

// 继续,Object.prototype对象也有__proto__属性,但它比较特殊,为null
Object.prototype.__proto__ === null // true

typeof null // "object"

这个由__proto__串起来的直到Object.prototype.__proto__ ==> null对象的链称为原型链(null没有原型,是这个原型链中的最后一个环节)。

  • yakima的__proto__属性指向Person.prototype对象;
  • Person.prototype对象的__proto__属性指向Object.prototype对象;
  • Object.prototype对象的__proto__属性指向null对象。

下面有一些比较特别的情况,看完忘掉就可以了,如果是在准备面试,最好别看,别看混了^_^。

Object是函数对象,是通过new Function()创建的,所以Object.__proto__指向Function.prototype:

Object.__proto__ === Function.prototype // true

Function是函数对象,是通过new Function()创建的,所以Function.__proto__指向Function.prototype。本类创建本类。。。大概类似是这么个意思——人是人他妈生的,妖是妖他妈生的。

Function.__proto__ === Function.prototype // true

另外:

Function.prototype.__proto__ === Object.prototype // true

四、constructor

原型对象都有个constructor属性,用来引用它的函数对象。这是一种循环引用。

Person.prototype.constructor === Person // true
Function.prototype.constructor === Function // true
Object.prototype.constructor === Object // true

五、new运算符

5.1、new运算符的使用示例

new运算符用于创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例,使用示例:

function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}

const car1 = new Car('Eagle', 'Talon TSi', 1993);

console.log(car1.make); // expected output: "Eagle"

5.2、new运算符的代码实现

new关键字会进行如下的操作:

  1. 创建一个空的简单JavaScript对象(即{});
  2. 为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象 ;
  3. 将步骤1新创建的对象作为this的上下文 ;
  4. 如果该函数没有返回对象,则返回this。

new的代码实现:

function myNew(Con, ...args) {
  // 创建一个新的空对象
  let obj = {};
  // 将这个空对象的__proto__指向构造函数的原型
  // obj.__proto__ = Con.prototype;
  Object.setPrototypeOf(obj, Con.prototype);
  // 将this指向空对象
  let res = Con.apply(obj, args);
  // 对构造函数返回值做判断,然后返回对应的值
  return res instanceof Object ? res : obj;
}

5.3、对new实现方式的验证

5.3.1、构造函数无返回值的情况(通常都是这种情况)

// 构造函数Person
function Person(name) {
  this.name = name;
}
let per = myNew(Person, '你好,new');
console.log(per); // {name: "你好,new"}
console.log(per.constructor === Person); // true
console.log(per.__proto__ === Person.prototype); // true

5.3.2、构造函数有返回值的情况(这时候需要使用返回值)

构造函数返回值是对象类型的情况:

function Person(name) {
  this.name = name;
  return {
    age: 22
  }
}
let per = myNew(Person, '你好,new');
// 当构造函数返回对象类型的数据时,会直接返回这个数据, new 操作符无效
console.log(per); // {age: 22}

构造函数返回值是基础类型的情况:

function Person(name) {
  this.name = name;
  return '十二点的程序员'
}
let per = myNew(Person, '你好,new');
// 而当构造函数返回基础类型的数据,则会被忽略
console.log(per); // {name: "你好,new"}

六、性能问题

在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。

遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。要检查对象是否具有自己定义的属性,而不是其原型链上的某个属性,则必须使用所有对象从 Object.prototype 继承的 hasOwnProperty (en-US) 方法。下面给出一个具体的例子来说明它:

console.log(g.hasOwnProperty('vertices'));
// true

console.log(g.hasOwnProperty('nope'));
// false

console.log(g.hasOwnProperty('addVertex'));
// false

console.log(g.__proto__.hasOwnProperty('addVertex'));
// true

注意:检查属性是否为 undefined 是不能够检查其是否存在的。该属性可能已存在,但其值恰好被设置成了 undefined。

七、错误实践:扩展原生对象的原型

经常使用的一个错误实践是扩展 Object.prototype 或其他内置原型。

这种技术被称为猴子补丁并且会破坏封装。尽管一些流行的框架(如 Prototype.js)在使用该技术,但仍然没有足够好的理由使用附加的非标准方法来混入内置原型。

扩展内置原型的唯一理由是支持 JavaScript 引擎的新特性,如 Array.forEach。

八、综合理解

原型和原型链是JS实现继承的一种模型。

var Animal = function () {}
var Dog = function () {}

Animal.price = 2000
Dog.prototype = Animal

var tidy = new Dog()

console.log(Dog.price) // undefined
console.log(tidy.price) // 2000

对上例的分析:

  • Dog自身没有price属性,沿着Dog.__proto__属性往上找,因为Dog的赋值是通过var Dog = function () {}实现的,所以Dog其实是使用new Function()创建的,所以Dog.__proto__ ==> Function.prototype,Function.prototype.__proto__ ==> Object.prototype,而Object.prototype.__proto__ ==> null。很明显,整条链上都找不到price属性,只能返回undefined;
  • tidy自身没有price属性,沿着tidy.__proto__属性往上找,因为tidy对象是Dog函数对象的实例,所以tidy.__proto__ ==> Dog.prototype ==> Animal,从而tidy.price获取到了Animal.price的值。

参考资料

赞(0) 打赏
文章名称:《JS原型与原型链》
文章链接:https://www.orzzone.com/js-prototype-and-prototype-chain.html
商业联系:yakima.public@gmail.com

本站大部分文章为原创或编译而来,对于本站版权文章,未经许可不得用于商业目的,非商业性转载请以链接形式标注原文出处。
本站内容仅供个人学习交流,不做为任何投资、建议的参考依据,因此产生的问题需自行承担。

评论 抢沙发

觉得文章有用就打赏一下文章作者

非常感谢你的打赏,我们将继续给力提供更多优质内容!

支付宝扫一扫打赏

微信扫一扫打赏

登录

找回密码

注册