前端面试必杀技:原型、原型链以及继承(一张图搞定面试)

4,942 阅读10分钟

对象基础

对象介绍

  • 什么是对象

    • 多个数据(属性)的集合;
    • 用来保存多个数据(属性)的容器;
  • 属性组成

    • 属性名:字符串(标识);
    • 属性值:任意类型;
  • 属性的分类:

    • 一般:属性值不是function,描述对象的状态;
    • 方法:属性值为function的属性,描述对象的行为;
  • 特别的对象

    • 数组:属性名是0,1,2,3之类的索引;
    • 函数:可执行的;
  • 对象是一种复合数据类型,可以保存不同类型的属性;

  • 创建对象

    var obj = new object();
    
  • 向对象中添加属性

    • .属性名;
    • ['属性名']:属性名有特殊字符/属性名是一个变量;
    obj.属性名 = 属性值;
    obj[‘属性名’] = 属性值;
    
    • 使用[]去操作属性时,[]中传递的是一个字符串
    • 能传字符串的地方就能传变量
    • 如果我们对象的属性名过于奇怪,则必须使用[]来操作。

对象创建模式

1.对象字面量模式

  • 套路: 使用{}创建对象, 同时指定属性/方法;
  • 适用场景: 起始时对象内部数据是确定的;
  • 问题: 如果创建多个对象, 有重复代码;
var p = {
 name: 'Tom',
  age: 23,
  setName: function (name) {
    this.name = name
  }
}
console.log(p.name, p.age)
p.setName('JACK')
console.log(p.name, p.age)
var p2 = {
  name: 'BOB',
  age: 24,
  setName: function (name) {
    this.name = name
  }
}

2.Object构造函数的模式

  • 套路: 先创建空Object对象, 再动态添加属性/方法
  • 适用场景: 起始时不确定对象内部数据;
  • 问题: 语句太多;
// 一个人: name:"Tom", age: 12
var p = new Object()
p = {}
p.name = 'Tom'
p.age = 12
p.setName = function (name) {
  this.name = name
}
p.setaAge = function (age) {
  this.age = age
}
console.log(p)

3.工厂模式

  • 套路: 通过工厂函数动态创建对象并返回;
  • 适用场景: 需要创建多个对象;
  • 问题: 对象没有一个具体的类型,都是Object类型;
// 工厂函数: 返回一个需要的数据的函数
  function createPerson(name, age) {
    var p = {
      name: name,
      age: age,
      setName: function (name) {
        this.name = name
      }
    }
    return p
  }
  var p1 = createPerson('Tom', 12)
  var p2 = createPerson('JAck', 13)
  console.log(p1)
  console.log(p2)

4.自定义构造函数模式;

  • 套路: 自定义构造函数,通过new创建对象;
  • 适用场景: 需要创建多个类型确定的对象;
  • 问题: 每个对象都有相同的数据, 浪费内存;将属性和方法添加到各个实例对象上去,但是每个实例都有相同的方法,重复了,我们可以将相同的方法放到他的构造函数的原型对象上去;
function Person(name, age) {
	  this.name = name
	  this.age = age
	  this.setName = function (name) {
	  this.name = name
    }
}
var p1 = new Person('Tom', 12)
var p2 = new Person('Tom2', 13)
console.log(p1, p1 instanceof Person)

对象高级

原型与原型链

什么是原型

1、prototype本质上还是一个JavaScript对象; 2、每个函数都有一个默认的prototype属性; 3、通过prototype我们可以扩展Javascript的内建对象

原型的扩展

  • 所有函数都有一个特别的属性:prototype显式原型属性(只有函数有prototype,对象是没有的。);
  • 所有实例对象都有一个特别的属性:__proto__隐式原型属性;
  • 原型是用于保存对象的共享属性和方法的,原型的属性和方法并不会影响函数本身的属性和方法。
  • 显式原型与隐式原型的关系
    • 函数的prototype:定义函数时被自动赋值,值默认为{},即原型对象;
    • 实例对象的_proto_: 在创建实例对象时被自动添加, 并赋值为构造函数的prototype值;
    • 原型对象即为当前实例对象的父对象;

原型链

  • 所有的实例对象都有__proto__属性, 它指向的就是原型对象
  • 这样通过__proto__属性就形成了一个链的结构---->原型链;
  • 当查找对象内部的属性/方法时, js引擎自动沿着这个原型链查找;
  • 当给对象属性赋值时不会使用原型链, 而只是在当前对象中进行操作;

原型链图

面试必画图

  • 图片是个人画的图,面试官但凡问到原型链问题,就可以画出此图,并且边画边叙述,会给你蹭蹭地加分哦;
  • 图片上有必说的语句,并且标明了结合画图时说这些语句的时机

原型链回答必杀图

new关键字做了什么

  • 示例:
// 构造函数
function Base(){}
var baseObj = new Base()
  • 创建了一个空对象;
  • 将这个空对象的隐式原型_proto_\指向构造函数的显示原型prototype;如例,是将空对象的__proto__成员指向了Base函数对象prototype成员对象;
  • 将构造函数的this指向实例(即空对象),并调用构造函数Base;
var obj  = {}; 
obj.__proto__ = Base.prototype; 
Base.call(obj);  
  • 根据new的工作原理手动实现一下new运算符
let newObj = function(func){
  //创建对象,错误示范:Object.create()方法创建一个新对象,使用现有的对象的prototype指向括号中的对象func.prototype。
  // let obj = Object.create(func.prototype)
  // 所以应该如下创造对象,是为了使新创建的对象的__proto__指向构造函数的原型func.prototype
  let obj = new Object()
  obj.__proto__=func.prototype
  // 将构造函数的作用域给新的对象,并且执行构造函数
  // 如果构造函数有返回值,那就返回返回值,如果没有,会返回undefined
  let k = func.call(obj)
  if(typeof k === 'object'){
    // 如果返回的类型是一个对象,那就返回该对象
    return k
  }else{
    // 如果构造函数执行后,返回的类型不是一个对象的话,那就返回创建的对象
    return obj
  }
}

对象的继承

复制属性式继承

// 创建父对象
var parentObj = {
	name: 'parentName',
	age: 25,
	showName:function(){
        console.log(this.name);
    }
}
// 创建需要继承的子对象
var childrenObj= {}
// 开始拷贝属性(使用for...in...循环)
for(var i in parentObj){
	childrenObj[i] = parentObj[i]
}
console.log(childrenObj); //{ name: 'parentName', age: 25, showName: [Function: showName] }
console.log(parentObj); // { name: 'parentName', age: 25, showName: [Function: showName] }
  • 重点:将父对象的函数和方法循环进行复制,复制到子对象里;
  • 缺点:如果继承过来的成员是引用类型的话,那么这个引用类型的成员在父对象和子对象之间是共享的,也就是说修改了之后, 父子对象都会受到影响。

原型继承://TODO

  • 原型式继承就是借用构造函数的原型对象实现继承,即 子构造函数.prototype = 父构造函数.prototype;
// 创建父构造函数
function Parent(){}
// 设置父构造函数的原型对象
Parent.prototype.age = 25;
Parent.prototype.friends = ['小名','小丽'];
Parent.prototype.showAge = function(){
    console.log(this.age);
};
// 创建子构造函数
function Child(){}
// 设置子构造器的原型对象实现继承
Child.prototype = Parent.prototype
// 因为子构造函数的原型被覆盖了, 所以现在子构造函数的原型的构造器属性已经不再指向Child,而是Parent。此时实例化Child和实例化parent的区别是不大的,所以再次创建Child是没有意义的,并且Child.prototype添加属性,也是会影响到Parent.prototype;
console.log(Child.prototype.constructor == Parent);// true
console.log(Parent.prototype.constructor == Parent);// true

// 问题就在这里!!!!
// 所以我们需要修正一下
Parent.prototype.constructor = Child;
// 上面这行代码之后, 就实现了继承
var childObj = new Child();
console.log(childObj.age);// 25
console.log(childObj.friends);// ['小名','小丽']
childObj.showAge();// 25
  • 问题:
    • 只能继承父构造函数的原型对象上的成员, 不能继承父构造函数的实例对象的成员;
    • 父构造函数的原型对象和子构造函数的原型对象上的成员有共享问题;

原型链继承 : 得到方法

// 定义父构造函数
function Parent(name,friends){
	this.name = name;
	this.friends = friends;
}
Parent.prototype.test = function(){
	console.log('原型方法', this.friends)
};
// 定义子构造函数
function Child(name,friends,age){
    this.age = '12'
}
// 将子构造函数的原型指定父函数的实例
Child.prototype = new Parent('parentName',['a','b','c']);
// 但是
console.log(Child.prototype.constructor); 
//输出:function Parent(){this.name = 'me';this.sex = ['male','female']}
// 所以,把Child的原型的构造函数修复为child
Child.prototype.constructor = Child
var childObj = new Child('childName',[3,4,'ddd'],24);//有test()

// 问题一:子实例无法向父类传值
console.log(childObj.name,childObj.friends) // parentName和["a", "b", "c"]
// 问题二:如果其中一个子类修改了父类中的引用数据类型的属性,那么就会影响其他的子类
var childObj2 = new Child('childName',[3,4],24);
childObj2.friends.push('additem')
console.log(childObj1.friends,childObj2.friends)//  ["a", "b", "c", "additem"], ["a", "b", "c", "additem"]
  • 重点:让新实例(继承对象childObj)的构造函数(Child)的原型等于父类的实例(被继承的实例 new Parent()),或者说将父类的实例作为子类的原型;
  • 特点:
    • 实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。
  • 缺点:
    • 1、新实例无法向父类的构造函数中传递参数。
    • 2、继承单一。
    • 3、所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)

  

借用构造函数call(经典继承) : 得到属性

  • 使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型);
  • 问题:Child无法继承Parent原型上的对象,并没有真正的实现继承(部分继承);
function Parent(xxx){this.xxx = xxx}
Parent.prototype.test = function(){};
function Child(xxx,yyy){
    Parent.call(this, xxx);
}
var child = new Child('a', 'b');  //child.xxx为'a', 但child没有test()
// 问题:
console.log(child.test);// undefined
  • 特点
    • 创建子类实例时,可以向父类传递参数
    • 可以实现多继承(call多个父类对象)

组合式继承

  • 借用构造函数 + 原型式继承
// 创建父构造函数
// 父类属性
function Parent(name){
	this.name = name;
	this.sex = ['male','female']
}
// 父类原型方法
Parent.prototype.test = function(){
	console.log(this.name)
};
// 定义子构造函数
function Child(name,age){
	// 复制父级构造函数的属性和方法
	// 使得每一个子对象都能复制一份父对象的属性且不会相互影响
    Parent.call(this,name);//继承实例属性,第一次调用Parent()
    this.age = age
}
// 将子构造函数的原型对象指向父级实例
var parentObj = new Parent();//继承父类方法,第二次调用Parent()
Child.prototype = parentObj; //得到test()
// 将子构造函数Child原型的构造函数修复为Child
Child.prototype.constructor = Child; 
var childObj = new Child('zhangsan',15); console.log(childObj,childObj.name,childObj.sex,childObj.test)
// 输出:childObj.name:'zhangsan';childObj.sex:["male", "female"];childObj.test:一个函数
  • 相当重要的一步:Child.prototype.constructor = Child;
    • 1.任何一个Prototype对象都有一个constructor指针,指向它的构造函数;
    • 2.每个实例中也会有一个constructor指针,这个指针默认调用Prototype对象的constructor属性。
    • 结果:当替换了子类的原型之后,即 Child.prototype = new Parent()之后,Child.prototype.constructor 就指向了Parent(),Child的实例的constructor也指向了Parent(),这就出现问题了。
    • 因为这造成了继承链的紊乱,因为Child的实例是由Child构造函数创建的,现在其constructor属性却指向了Parent,为了避免这一现象,就必须在替换prototype对象之后,为新的prototype对象加上constructor属性,使其指向原来的构造函数。
  • 缺点:通过将子构造函数的原型指向父构造函数的实例,会两次调用父类构造函数;

寄生组合式继承

  • 原理:通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。
  • 思路:不必为了指定子类的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。
  • 寄生组合式继承就是为了降低调用父类构造函数的开销而出现的 ;
  • 本质上,就是使用寄生式继承来继承父类型的原型,然后再将结果指定给子类型的原型。
  • 解决方法是在中间架一座桥梁,加一个空的构造函数;
// 创建父构造函数
function Parent(){
	this.name = 'me';
	this.sex = ['male','female']
}
Parent.prototype.test = function(){};
// 定义子构造函数
function Child(){
	// 复制父级构造函数的属性和方法
	// 使得每一个子对象都能复制一份父对象的属性且不会相互影响
    Parent.call(this);
    this.age = '12'
}

// 定义空函数
function F(){}
// 把空函数的原型指向Parent.prototype
// 寄生式组合继承
F.prototype = Parent.prototype

// 将子构造函数的原型对象指向空函数F的实例对象fObj
var fObj = new F();
Child.prototype = fObj; 

// 将子构造函数Child原型的构造函数修复为Child
Child.prototype.constructor = Child; 
var childObj = new Child(); 
  • 优点:高效率体现在只调用了一次Parent构造函数,并且因此避免了在Child.prototype上面创建不必要的,多余的属性。与此同时,原型链还能保持不变;因此,还能正常使用instanceof 和 isPrototypeOf()。
  • 开发人员普遍认为寄生式组合式继承是引用类型最理想的继承范式。