阅读 881

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

对象基础

对象介绍

  • 什么是对象

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

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

    • 一般:属性值不是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运算符
var new2 = function (func) {
  //创建对象,Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
  var o = Object.create(func.prototype); 
  //改变this指向,把结果付给k   
  var k = func.call(o);
  //判断k的类型是不是对象
  if (typeof k === 'object') {
	 //是,返回k
     return k;
  } else {
	 //不是返回返回构造函数的执行结果
     return o;
  }
}    
复制代码

应对面试

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

原型链回答必杀图

对象的继承

复制属性式继承

// 创建父对象
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] }
复制代码
  • 重点:将父对象的函数和方法循环进行复制,复制到子对象里;
  • 缺点:如果继承过来的成员是引用类型的话,那么这个引用类型的成员在父对象和子对象之间是共享的,也就是说修改了之后, 父子对象都会受到影响。

原型继承:(不准确,待定问题)

  • 原型式继承就是借用构造函数的原型对象实现继承,即 子构造函数.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(){
	this.name = 'me';
	this.sex = ['male','female']
}
  
Parent.prototype.test = function(){};
// 定义子构造函数
function Child(){
    this.age = '12'
}
// 将子构造函数的原型指定父函数的实例
Child.prototype = new Parent();
// 但是
console.log(Child.prototype.constructor); 
//function Parent(){
//	this.name = 'me';
//	this.sex = ['male','female']
//}
// 所以,把Child的原型的构造函数修复为child
Child.prototype.constructor = Child
var childObj = new Child(); //有test()
复制代码
  • 重点:让新实例(继承对象childObj)的构造函数(Child)的原型等于父类的实例(被继承的实例 new Parent()),或者说将父类的实例作为子类的原型;
  • 特点:
    • 实例可继承的属性有:实例的构造函数的属性,父类构造函数属性,父类原型的属性。(新实例不会继承父类实例的属性!)
  • 缺点:
    • 1、新实例无法向父类构造函数传参。
    • 2、继承单一。
    • 3、所有新实例都会共享父类实例的属性。(原型上的属性是共享的,一个实例修改了原型属性,另一个实例的原型属性也会被修改!)

  

借用构造函数 : 得到属性

  • 使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型);
  • 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()
复制代码
  • 特点
    • 创建子类实例时,可以向父类传递参数
    • 可以实现多继承(call多个父类对象)

组合继承

  • 借用构造函数 + 原型式继承
// 创建父构造函数
function Parent(){
	this.name = 'me';
	this.sex = ['male','female']
}
Parent.prototype.test = function(){};
// 定义子构造函数
function Child(){
	// 复制父级构造函数的属性和方法
	// 使得每一个子对象都能复制一份父对象的属性且不会相互影响
    Parent.call(this);
    this.age = '12'
}
// 将子构造函数的原型对象指向父级实例
var parentObj = new Parent();
Child.prototype = parentObj; //得到test()
// 将子构造函数Child原型的构造函数修复为Child
Child.prototype.constructor = Child; 
var childObj = new Child(); //childObj.xxx为'a', 也有test()
复制代码
  • 缺点:通过将子构造函数的原型指向父构造函数的实例,会两次调用子构造函数;

寄生组合式继承

  • 寄生组合式继承解决了两次调用的问题,组合式继承会有两次调用的情况;
  • 解决方法是在中间架一座桥梁,加一个空的构造函数;
// 创建父构造函数
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(); 
复制代码
关注下面的标签,发现更多相似文章
评论