JavaScript原型及原型链

156 阅读5分钟

原型

原型:是function对象的一个属性,它定义了构造函数制造出的对象的公共祖先。通过该构造函数产生的对象,可以继承该原型的属性和方法(原型也是对象)。

属性用法描述
prototype构造函数名.prototype可以理解为函数的原型对象
proto对象名.proto指向该对象的原型
constructor对象名.constructor指向关联的构造函数,实例原型指向构造函数

原型链

原型链:当对象找不到需要的属性时,它会到这个对象的父对象上去找,以此类推,这就构成了对象的原型链。并且Object.prototype是对象的最终原型绝大多数对象最终都会继承自Object.prototype,而Object.prototype的原型是null

理解概念

  • JavaScript分为函数对象普通对象,每个对象都有__proto__属性,但是只有函数对象才有prototype属性;(准则一)
  • Object、Function都是JavaScript内置的函数,类似的还有我们常用的Array、RegExp、Date、Boolean、Number、String;(准则一)
  • 属性__proto__是一个对象,它有两个属性。contructor和__proto__;(准则二)
  • 原型对象prototype有一个默认的constructor属性,用于记录实例是由哪个构造函数创建。(准则二)
  • 不管有没有__proto__属性,JavaScript标准规范定义了prototype原型属性,所有对象都使用由构造函数所指向的原型对象。原型对象,默认情况下,都会有一个constructor属性,重新指回构造函数。

  • 对象的原型仅用于属性继承。函数本身并不使用其关联的原型对象,但是因为函数本身也是一个对象,它会继承它的构造函数的原型,JavaScript 中函数的构造函数,是Function对象

  • 每个原型对象自身,默认都是通过 new Object() 构造函数创建的,所以原型对象自身(比如Foo.prototype)的原型对象(其__proto__指向)就是Object.prototype。因此,不管任意类型的实例,最后都会从Object.prototype中继承属性。

    • Object.prototype的原型对象(__proto__)是null。常见错误:Cannot read properties of null,是不是似曾相识,因为null没有原型对象,就不能继续通过原型链往上查找属性了,直接异常。
  • 所以对象会自动从原型链中读取属性,就好像属性定义在对象自身上。直接在实例自身上设置原型链中同名的属性,会隐藏/遮蔽原型链中的同名的那个属性。

// 从上方 function Foo() 开始分析这一张经典之图
function Foo()
let f1 = new Foo();
let f2 = new Foo();

f1.__proto__ = Foo.prototype; // 准则2
f2.__proto__ = Foo.prototype; // 准则2
Foo.prototype.__proto__ = Object.prototype; // 准则2 (Foo.prototype本质也是普通对象,可适用准则2)
Object.prototype.__proto__ = null; // 原型链到此停止
Foo.prototype.constructor = Foo; // 准则1
Foo.__proto__ = Function.prototype; // 准则2
Function.prototype.__proto__  = Object.prototype; //  准则2 (Function.prototype本质也是普通对象,可适用准则2)
Object.prototype.__proto__ = null; // 原型链到此停止
// **此处注意Foo 和 Function的区别, Foo是 Function的实例**

// 从中间 function Object()开始分析这一张经典之图
function Object()
let o1 = new  Object();
let o2 = new  Object();

o1.__proto__ = Object.prototype; // 准则2
o2.__proto__ = Object.prototype; // 准则2
Object.prototype.__proto__ = null; // 原型链到此停止
Object.prototype.constructor = Object; // 准则1
// 所有函数的__proto__  都和 Function.prototype指向同一个地方
Object.__proto__ = Function.prototype // 准则2 (Object本质也是函数);
// 此处有点绕
Function.prototype.__proto__ =  Object.prototype; // 准则2 (Function.prototype本质也是普通对象,可适用准则2)
Object.prototype.__proto__ = null; // 原型链到此停止

// 从下方 function Function()开始分析这一张经典之图
function Function()
Function.__proto__ = Function.prototype // 准则2
Function.prototype.constructor = Function; // 准则1

案例

案例一

let user = {
  name: 'John',
  surname: 'Smith',

  set fullName(value) {
    // 理解这一步是关键
    [this.name, this.surname] = value.split(' ');
  },

  get fullName() {
    console.log('get', this);
    return `${this.name} ${this.surname}`;
  }
};

let admin = {
  __proto__: user,
  isAdmin: true
};

console.log(admin.fullName); // John Smith (*)

// setter triggers!
admin.fullName = 'Alice Cooper'; // (**)

console.log(admin.fullName); // Alice Cooper,admin 的内容被修改了
console.log(user.fullName); // John Smith,user 的内容被保护了

上述的输出结果,感觉主要是去考察自身属性和原型上的属性的处理过程,在上述代码中的setter方法中,通过直接赋值的方式给this对象绑定属性值,这一步是直接去赋值,而不需要去查找原型上是否有对应的属性,然后才去赋值。同时这里也需要去理解this的指向。

案例二

let hamster = {
  stomach: [],

  eat(food) {
    console.log(this);
    this.abc = '123';
    // this.stomach = '123';
    this.stomach.push(food);
  }
};

let speedy = {
  __proto__: hamster,
  hello: ''
};

let lazy = {
  __proto__: hamster
};

// 这只仓鼠找到了食物
speedy.eat('apple');
console.log(speedy.stomach); // apple

// 这只仓鼠也找到了食物,为什么?请修复它。
console.log(lazy.stomach); // apple
console.log(lazy.stomach); // apple

这个案例不仅是考察了给原型对象上的属性赋值后,会影响所有实例对象访问该原型属性,还考察了什么时候会去修改原型上的属性值。在这里的 this.stomach.push(food);,它首先会去查找this对象上是否有这个stomach属性,如果有就会去看属性值是否有push方法,然后就进行后续操作;如果没有,那么就要去递归原型链,查看是否有对应的属性,如果找到就判读是否有push方法,然后进行后续操作,如果没有就报错;所以这里最终会影响原型上的属性值,故而两个都输出相同的结果。(这里叙述得比较泛,读者需要去仔细理解访问对象属性和修改对象属性的过程)

原型链继承

在ES5的时候,JavaScript还没有Class类的使用,要实现对象属性的继承就可以通过原型链的方式来实现。

这里可以参考个人文章:JavaScript原型链继承

引用文章