原型链一文通

原型链基本概念

基本思想:利用原型链让一个引用类型继承另一个引用类型的属性和方法。
原型链:每一个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针,让原型对象等于另一个类型的实例,层层递进,直到指向Object对象为止,就构成了实例与原型的链条。(Object是原型链的顶端。Object.prototype._proto_为null )

  • 原型:原型是一个prototype对象,用于表示类型之间的关系。(为每个实例对象存储共享的方法和属性)
  • 构造函数
  • 实例

原型、构造函数、实例之间的关系

function Person () {}
const person = new Person();
console.log(person.__proto__ === Person.prototype); // true
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.constructor === Person); // true

上面代码person作为Person的实例对象,Person作为Object的实例对象

在这里插入图片描述

函数对象Function与Object

在这里插入图片描述
每个对象都会有一个原型,就是[[prototype]],在ES规范里该属性是隐藏的,但在浏览器中则以__proto__的形式暴露出来

  • Object是构造函数,且所有的构造函数都是Function的实例,所以
    Object.__proto__ === Function.prototype
  • 由于Function本身也是一个函数,所以
    Function.__proto__ === Function.prototype
  • Function.prototype是一个对象,所以
    Function.prototype.__proto__ === Object.prototype
  • Object.prototype.__proto__是顶级对象,所以
    Object.prototype.__proto__ === null

_proto_和prototype的区别

_proto_:指向原型对象
prototype:指向一个有constructor属性的原型对象。

实现继承,es5和es6

1. 原型链继承
  • 优点:
    • 简单易实现
    • 父类新增原型方法/原型属性,子类都能访问
    • 实例是子类的实例也是父类的实例
  • 缺点:
    • 为子类新增属性和方法,不能在构造函数中
    • 无法实现多继承
    • 创建子类实例时,不能向父类构造函数传参数
    • 所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型上引用类型的属性,另一个实例的原型属性也会被修改!)
function Person(name) {
  this.name = name
  this.say = function () {}
}
Person.prototype.listen = function () {}

function Student() { }

Student.prototype = new Person()  //关键
2. 借用构造函数继承
  • 优点:
    • 解决了原型链继承中构造函数引用类型共享的问题
    • 可以向构造函数传参(通过call传参)
  • 缺点:
    • 父类的原型方法不会被子类继承
    • 方法都在构造函数中定义,每次创建实例都会创建一遍方法。
function Person(name) {
  this.name = name
  this.say = function () {}
}
Person.prototype.listen = function () {}
function Student() {
  Person.call(this) // 关键
}
let st1 = new Student();
st1.listen() //报错,listen is undefined
3.组合继承(原型链继承 + 构造函数继承)
  • 缺点:
    • 父类的构造函数执行了两遍:一次在子类的构造函数中call方法执行一遍,一次在子类原型实例化父类的时候执行一遍。
function Person(name) {
  this.name = name
  this.say = function () {}
}
Person.prototype.listen = function () {}
function Student() {
  Person.call(this) // 关键
}
Student.prototype = new Person()
4.原型式继承(es5 Object.create)
  • 缺点:包含引用类型的属性值始终都会共享相应的值
function object(obj){
  function F(){}
  F.prototype = obj;
  return new F();
}
var person = {
  name: "person",
  friends: ["a", "b", "c"]
};
let person1 = object(person);
person1.name = "person1";
person1.friends.push("d");
let person2 = object(person);
person2.name = "person2";
person2.friends.push("e");
console.log(person1.name) // person1
console.log(person1.friends) // [a,b,c,d,e]
5. 寄生式继承
  • 优点:函数的主要作用是为构造函数新增属性和方法,以增强函数
  • 缺点:包含引用类型的属性值始终都会共享相应的值
function object(obj){
  function F(){}
  F.prototype = obj;
  return new F();
}
 //寄生式继承
function changeData(obj) {
  const newData = object(obj);
  newData.age = 23;
  return newData;
}
let person = {
  name: "person",
  friends: ["a", "b", "c"]
};
let person1 = changeData(person);
6. 寄生组合式继承(借用寄生 + 组合继承),最常用最优的继承方式
  • 优点:解决了组合继承两次调用父类构造函数的问题
// 原型式继承
function object(obj){
  function F(){}
  F.prototype = obj;
  return new F();
}
function inherit(child, parent){ // 核心逻辑
  let prototype = object(parent.prototype)
  prototype.constructor = child; 
  child.prototype = prototype; // 核心,这里解决了两次调用父类构造(这里没有调用)
}
function Person(name) {
  this.name = 'person name'
  this.say = function () {
    console.log('person say')
  }
}
Person.prototype.listen = function () {
  console.log('person.prototype listen')
}

function Student() {
  Person.call(this) // 构造函数继承
}
inherit(Student, Person)

let stu1 = new Student();
console.log(stu1, stu1.name) // Student {name: 'person name', say: ƒ} 'person name'
stu1.say() // person say
stu1.listen() // person.prototype listen
7. es6 extends
  • 子类必须在constructor()方法中调用super(),否则就会报错(这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法。如果不调用super()方法,子类就得不到自己的this对象)
  • 可以不显式的写constructor(),如果不写就会隐性添加,然后调用super()
  • 新建子类实例时,父类的构造函数必定会先运行一次
  • 父类的私有属性和私有方法无法被子类继承(#定义的,比如#p=1,p属性无法被继承)
  • 静态属性和静态方法的继承(static定义的),静态属性是浅拷贝
  • Object.getPrototypeOf(a) === Person
class Person {
	constructor() {
		console.log('Person constructor');
	}
}

class Student extends Person {
  constructor() {
    super(); // 调用父类的constructor
	console.log('Student constructor');
  }
}
let a = new Student()
// 会依次输出 Person constructor 
// Student constructor
8. es6 extends与es5 的继承方式有什么区别?
  • ES5 的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即“实例在前,继承在后”。
  • ES6 的继承机制,则是先将父类的属性和方法,加到一个空的对象上面,然后再将该对象作为子类的实例,即“继承在前,实例在后”。这就是为什么 ES6 的继承必须先调用super()方法,因为这一步会生成一个继承父类的this对象,没有这一步就无法继承父类。

instanceOf是如何进行判断对象类型的

1. instanceOf概念

用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
在这里插入图片描述
通过判断实例的_proto_属性与构造函数的prototype属性是否指向同一个原型对象。
注意1: 实例的_proto_指向的是构造函数的prototype属性,与构造函数没有关系。
注意2: 原型上面可能还有会有原型,会沿着原型链继续向上找,找到返回true,反之返回false。

2. 原生实现
function myInstanceOf(left,right){
	if (typeof left !== 'object' || left === null) return false;
	let prototype = right.prototype; // 拿到right的原型
	// 或者可以用Object.getPrototypeOf(left)
	let proto = left.__proto__; // left作为实例,拿到left的原型
	while(true){
	    if(proto === prototype) return true;
	    if(proto === null) return false;
	    //若本次查找无结果,则沿着原型链向上查找
	    proto = proto.__proto__;
	}
}
let a = [1,2,3];
console.log(myInstanceOf(a,Array));
//true
  • 举一个栗子:
foo instanceof Foo    //结果为true,说明foo._proto_ == Foo.prototype;

但是我们不能轻易说明fooObject的实例,只有以下满足,才是Object的实例。

foo instanceof Object   //结果为true,说明Foo.prototype._proto_ == Object.prototype
3. 想一想:如果A继承B,B继承C,C继承D,那么怎么判断a是由A直接生成的实例还是B直接生成的?

a._proto_.constructor == B 通过constructor来判断,a由B直接生成

4. instanceof能否判断基本数据类型?
class PrimitiveNumber {
  static [Symbol.hasInstance](x) {
    return typeof x === 'number'
  }
}
console.log(111 instanceof PrimitiveNumber) // true

new运算符

  1. 创建一个空对象obj,并让其继承func.prototype
  2. 执行构造函数,并将this指向创建的空对象obj
  3. 返回创建的对象obj,new一个实例的时候,如果没有return,就会根据构造函数内部this绑定的值生成对象,如果有返回值,就会根据返回值生成对象,为了模拟这一效果,就需要判断apply后是否有返回值
1.原生实现new操作
function myNew(func, ...args){
  //1.创建一个空对象obj,并让其继承func.prototype
  //等同于let obj={};obj._proto_ = func.prototype;
  let obj = Object.create(func.prototype); 
  //2.执行构造函数,并将this指向创建的空对象obj
  let result = func.apply(obj,arguments);
  //3.返回创建的对象obj,new一个实例的时候
  //如果没有return,就会根据构造函数内部this绑定的值生成对象,
  //如果有返回值,就会根据返回值生成对象,为了模拟这一效果,就需要判断apply后是否有返回值
  return obj instanceof Object ? result : obj;
}
console.log(myNew(Person, 'HANMEI'));

Object.create(),实现原理和继承,与 new Object 的区别

1. 基本用法

Object.create(proto, propertiesObject)

  • proto 新创建对象的原型对象
  • propertiesObject,属性对应于 Object.defineProperties() 的第二个参数
    // 创建一个以o1为原型,增加b属性的对象 o2
    let o1 = {a: 'a'}
    let o2 = Object.create(
      o1,
      {
        b: {
          value: 'b',
          writable: true,
          enumerable: true,
          configurable: true,
        },
      },
    );
    
2. 类式继承
3. 与new Object 的区别
  • 创建对象的方式不同
    • new Object() 通过构造函数来创建对象, 添加的属性是在自身实例下
    • Object.create()是 ES6 创建对象的另一种方式,可以理解为继承一个对象, 添加的属性是在原型下,传入的第一个参数是直接作为新对象的原型对象的,无法通过 instanceof 运算符来判断其类型
  • 创建空对象时不同
    • 当用构造函数或对象字面量方法创建空对象时,对象时有原型属性的,即有 __proto__;
    • 当用 Object.create() 方法创建空对象时,对象是没有原型属性的。
// new Object() 方式创建
let a = {  rep : 'apple' }
let b = new Object(a)
console.log(b) // {rep: "apple"}
console.log(b.__proto__) // {}
console.log(b.rep) // apple

// Object.create() 方式创建
let c = Object.create(a)
console.log(c)  // {}
console.log(c.__proto__) // {rep: "apple"}
console.log(c.rep) // apple
4. 实现原理
Object.cteate = function(proto) {
  function F() {}
  F.prototype = proto
  return new F()
}

面试题

1.代码输出题(构造函数和实例对象的原型链问题)
Function.prototype.a = () => {
  console.log(1);
}
Object.prototype.b = () => {
  console.log(2);
}
function A() {}
const a = new A();

a.a(); // 报错
a.b(); // 2
A.a();  // 1
A.b(); // 2

解析:对于a,作为A的实例对象,会沿着原型链向上查找,查找顺序为:

  • a
  • a.__proto__也就是A.prototype
  • A.prototype.__proto__ 也就是Object.prototype
  • Object.prototype.__proto__也就是null,因为Object.prototype.__proto__ === null

对于A

  • A
  • A.__proto__也就是Function.prototype
  • Function.prototype.__proto__也就是Object.prototype
  • Object.prototype.__proto__ 也就是null
2. 代码输出题(原型链上的属性共享问题)
function A() {
}
A.prototype.n = 0;
A.prototype.add = function () {
  this.n += 1;
}

let a = new A();
let b = new A();
a.add();
console.log(b.n) // 0

解析:new之后的实例的this指向的是实例,所以当a执行add()的时候,相当于给a自身加一个属性为n,值为1,原型链上的n还是0,b自身没有n属性,所以去原型上找,得到0在这里插入图片描述

3.代码输出题(两个实例之间的属性共享问题)
function Person(name, age) {
  this.name = name;
  this.age = age;
  this.eat = function() {
    console.log(age + "岁的" + name + "在吃饭。");
  }
}
Person.run = function () {}
Person.prototype.walk = function () {}
let p1 = new Person("jsliang", 24);
let p2 = new Person("jsliang", 24);

console.log(p1.eat === p2.eat); // false,
console.log(p1.name === p2.name); // true,
console.log(p1.run === p2.run); // true,
console.log(p1.walk === p2.walk); // true

解析:

  • new之后,相当于把所有属性都重新拷贝了一份给实例,对于引用对象来说,新开辟的空间地址肯定不相同
  • 简单类型进行比较,‘jsliang’ === ‘jsliang’
  • run方法只是作为Person自己的静态属性,不给实例共享,所以为undefined === undefined
  • 原型上的方法是所有实例共享的
4.按照如下要求实现Person 和 Student 对象
  • Student 继承Person
  • Person 包含一个实例变量 name, 包含一个方法 printName
  • Student 包含一个实例变量 score, 包含一个实例方法printScore
  • 所有Person和Student对象之间共享一个方法
// 原生
function Person(name) {
  this.name = name;
  this.printName = function() {
  }
}
Person.prototype.commonMethods = function() {
}
function Student(score) {
  Person.call(this,score);
  this.score = score;
  this.printScore = function() {
  }
}
Student.prototype = new Person();
let a = new Student('11')
let b = new Student('22')
console.log(a.commonMethods === b.commonMethods) // true
// es6
class Person {
  constructor(name) {
     this.name = name;
  }
  printName() {}
  commonMethods(){}
}
class Student extends Person {
  constructor(name, score) {
    super(name);
      this.score = score;
    }
  printScore() {}
}
let stu = new Student('小红');
let person = new Person('小紫');
console.log(stu.commonMethods === person.commonMethods); //true
5. 代码输出题(原型和实例方法共享问题)
function Parent(){
   this.a = 'Parent'
}
function Tom() {
   this.a = 'Tom'
}
Parent.__proto__.print = function(){ // Parent.__proto__ === Function.prototype
   console.log(this.a)
}
Parent.print() // undefined
Tom.print() // undefined, print方法添加到了Function的原型上,所以可以访问到

var child = new Parent()
child.print() // 报错 ,Parent.prototype.__proto__ === Object.prototype
// Parent构造函数没有print方法,构造函数的原型上也没有,原型的原型上也没有,找到了终点都没找到,所以报错
6. 代码输出题(构造函数和原型对象方法共享问题, Object和Function)
function Test() {}  // Test是通过Function实例化出来的

Object.prototype.printName = function() {
 console.log('Object');
}

Function.prototype.printName = function() {
 console.log('Function');
}

Test.printName(); // Function

var obj = new Test();
obj.printName(); // Object  
// Test构造函数上没有这个方法,会去Test原型对象上找,原型对象也没有,原型对象的原型是Object.prototype
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值