Js中的类与继承

246 阅读10分钟

js中的类

prototype的由来

我们都知道对象可以通过new运算符加构造函数生成,但生成的这些对象彼此之间却没有任何联系,也就不能说明这些新创建的对象是属于某一类型的;

function People(name, age) {
	this.name = name;
  this.age = age;
  this.sayHi = function() {
    console.log(`Hi i am ${this.name}`)
  }
}

var xm = new People("小明", 17)
var xh = new People("小红", 17)
//更改其中一个实例对象的属性,不会影响其他的实例对象属性
xm.age = 20

xh.age // 17
xh.sayHi === xm.sayHi //false

除了彼此之间没有关联之外, 每次新创建的对象都具有独立的副本,更改其中一个对象的属性,其他对象不受影响;但是对于那些共有属性却无法复用,多次创造多次生成,造成系统资源的浪费;

为了解决上面提到的两个不足之处,js为构造函数引入prototype属性,该属性指向一个对象,通常我们将其称为原型对象;将实例的共有属性都放在原型对象身上, 那些不需要共有的属性都放在构造函数内。这样在实例创建后,就会有两类属性,一类自有属性, 一类继承自原型对象的属性;

function People(name, age) {
	this.name = name;
  this.age = age;
}
People.prototype.sayHi = function() {
  console.log(`Hi i am ${this.name}`)
}
var xm = new People("小明", 17)

Javascript继承机制的设计思想就是,原型对象上的所有属性和方法,都能被实例对象所共享。这样实例对象之间不仅有了关联,还节省了内存;Javascript也为对象提供了一些属性和方法用于获取实例对象的原型;

Object.getPrototypeOf()方法返回参数对象的原型。

function F() {}
var f = new F()
Object.getPrototypeOf(f) //{constructor: ƒ}
Object.getPrototypeOf(f) === F.prototype  //true

__proto__ :一个非标准属性,也可以用于获取对象的原型

function F() {}
var f = new F()
f.__proto__ === Object.getPrototypeOf(f) //true
f.__proto__ === F.prototype //true

constructor: 对象都有一个constructor属性,指向生成该实例对象的构造函数

function F() {}
var f = new F()
f.constructor === F //true
f.constructor.prototype === f.__proto__ //这也是一种获取对象原型的方法

prototype__proto__constructor三者之间关系

prototype是构造函数的属性,是函数才有的属性;指向一个含有constructor属性的对象,其中constructor指向构造函数本身,该对象中的所有属性和方法都被其实例对象所共享。

__proto__是对象的属性,指向对象的原型;他的作用在于当我们访问对象obj的一个属性时,如果该属性当前对象中不存在,他会继续在该对象的原型对象obj.__proto__中寻找;如果还没有,则继续往上在该原型对象的原型对象obj.__proto__.__proto__中寻找;如果一直没有该属性,会沿着这条链一直寻找到原型对象为null的原型对象Object.prototype中,这条链就是我们所谓的原型链

constructor是对象的属性, 指向生成该对象的构造函数

由于函数也是对象,所以函数也有__proto__constructor属性

下面是在网上找的一张很经典的三者关系的图

1200689-20170719140835833-1989846712

new 关键字

new运算符后面跟一个构造函数可以生成一个实例对象,那么在new的背后都做了哪些事呢?先尝试几个特列:

  1. 如果构造函数不用new执行而是直接调用会有什么问题?

    var xm = People("小明", 17) // 返回undefined
    

    由于构造函数本质上还是一个函数,当不使用new命令执行时,就会作为普通函数被调用;那么普通函数内部的this在非严格模式下的浏览器环境中指代全局对象window,那么函数执行后,构造函数内在this上相关的操作就等同于在全局对象window上的操作;

  2. 如果构造函数内部有return语句,结果会怎样?

    //构造函数内没有return语句时:
    function People(name, age) {
    	this.name = name;
      this.age = age;
    }
    var xm = new People("小明", 17) //People {name: "小明", age: 17}
    
    //内部return 基本数据类型 'far':
    var xm = new People("小明", 17) //{name: "小明", age: 17}
    
    //内部return 数组类型 ['far']:
    var xm = new People("小明", 17) //["far"]
    
    //内部return 函数  () => {}:
    var xm = new People("小明", 17) // () => {}
    
    //内部return 对象 {type: "object"}:
    var xm = new People("小明", 17) //{type: "object"}
    

    如果构造函数内部有return语句,当返回的是基本数据类型时,new命令会忽略掉该值,直接用新的实例对象this替代该值返回;当return后面是复杂数组类型时, 会直接将该值返回;

    new命令背后逻辑

      1. 先创建一个空对象,作为要返回对象的实例
      2. 将该空对象的原型属性`__proto__`指向构造函数的原型对象`prototype`
      3. 将该空对象赋值给构造函数内部的this
      4. 执行构造函数内部逻辑
      5. 将上面创建的对象实例返回
    

ES6中的class

在ES5中生成实例对象的传统方法是通过构造函数,使用new命令 创建一个实例对象;ES6引入class关键字用来定义类,实际上class可以看作是语法糖,完全可以通过ES5的方法实现。新的class写法只是让实例的原型更加清晰直观。

//es6
class Animal {
	constructor(name, age) {
    this.name = name;
    this.age = age
  }
  sayHello() {
    console.log(`Hi, my name is ${this.name}, i am ${this.age}`)
  }
}

//es5的继承
function Animal(name, age) {
  this.name = name;
  this.age = age
}
Animal.prototype.sayHello = function() {
   console.log(`Hi, my name is ${this.name}, i am ${this.age}`)
} 

在ES5中,构造函数的prototype属性指向一个含有constructor属性的对象,该对象内还包含了实例对象的共有属性和方法。这正好与ES6中通过class声明的对象一致; ES5中的构造函数就是ES6Class中的constructor,都返回新对象的实例this。通过new命令生成对象实例时,会自动调用constructor方法,如果class中没有该方法,那么一个默认的空constructor就会被自动创建,因为需要该方法返回实例对象。

上面说到class中定义的所有方法都会被实例继承, 但是如果在方法前有一个static关键字的静态方法,只能被该类调用,且实例不会继承,同时当静态方法中有this时,该this指向类本身,而不是实例。除了静态方法外,类还有静态属性,但是目前静态属性的写法不同与静态方法。

class F{
  hello() {
    console.log("prototype function")
  }
  static hi() { //静态方法,不会被实例继承,只能有类自己调用
    this.hello() //this代表类本身
    console.log("static function")
  }
}
F.name = "static property" //静态属性
类的区别ES5ES6
定义方式通过函数定义通过关键字class定义
严格模式内部非严格模式同模块一样内部默认都是严格模式
声明提升同函数声明一样,函数名、函数体提前不存在提升, 类必须先声明后使用
this指向新创建的实例在静态方法中指向类本身,其他情况指向实例
可枚举父类原型中的属性可枚举父类原型中的属性不可枚举

js中的继承

ES5中的继承

ES5中实现对象的继承主要围绕构造函数及对象的原型来实现, 从而达到一个类拥有另一个类属性的目的

function Animal() {
	this.type = "动物"
}
Animal.prototype.sayHi = function() { console.log("Hi", this.name) }

function Cat(name) {
	this.name = name
}
// 假如这里想让Cat的实例也能够拥有type的属性,该如何操作???

1.原型继承

让子类的原型prototype指向父类的实例

Cat.prototype = new Animal()
var cat = new Cat("小猫")
cat.type  //动物

原型继承就是让子类构造函数的prototype指向父类的实例,这样新创建的子类实例对象所拥有的属性包括:父类构造函数中的属性,父类构造函数原型中的属性, 子类构造函数中的属性,子类构造函数原型中的属性。

上面我们提到过构造函数的prototype指向一个包含constructor属性的对象, 而constructor指向构造函数本身。当我们直接给prototype赋值时,就切断了原来的prototype的指向, 重新替换了一个新的对象,这会丢失一些属性导致继承关系的混乱

cat.constructor === Animal //true
cat.constructor === Cat //false
//`cat`明明是通过构造函数`Cat`生成,但现在他的构造函数却指向`Animal`
cat instanceof Cat //true
cat instanceof Animal //true
//通过`instanceof`运算符计算又发现`cat`即是`Cat`的实例,又是`Animal`的实例

那上面的这两个现象不是自相矛盾吗?之前这里我经常会混淆。这次也顺带解释一下

image-20200826195309012

Cat.prototype = new Animal()

这一步直接切断了原有的指向,更改了构造函数的原型;由于构造函数的prototype与实例的——proto__指向同一个引用,所以catconstructor最终指向构造函数Animal()原型中的constructor

因此在直接替换构造函数的原型prototype时, 后面一定要手动更改其原型对象中的constructor

Cat.prototype.constructor = Cat

instanceof运算符本身是用于做检测类型,判断一个对象是否属于某一个类,由于它的内部是通过原型判断,

function instanceof(O, F) { //O一个对象, F 构造函数
  while(true) {
    if( O.__proto__ === F.prototype ) return true
    if( O.__proto__ === null ) return false
    O = O.__proto__ //沿着原型链不断向上查找
  }
}
cat.__prototype === Animal.prototype
cat.__prototype === Cat.prototype

特点: 实现简单,通过修改原型链实现继承

缺点: 1. 不可以向父类传参

  			2. 子类原型指向父类实例, 如果修改子类原型中的属性,该父类实例(**当前引用的**)属性也会改变
  			3. 继承链紊乱,需要手动修改子类原型的`constructor`

2.构造函数继承

在子类的构造函数中调用父类构造函数(执行父构造函数中的逻辑)

function Animal(type) {
	this.type = type
}
Animal.prototype.sayHi = function() { console.log("Hi", this.name) }

function Cat(name) {
  Animal.call(this,"Cat") //父类构造函数在这里被当作普通函数调用, this在这里指代将要创建的Cat实例对象
	this.name = name
}
var cat = new Cat("小猫")
cat.type // Cat
cat.name //小猫
cat.sayHi() // Uncaught TypeError: cat.sayHi is not a function
//cat.__proto__ ----> Cat.prototype ----> Function.prototype ----> Object.prototype
//animal.__proto__ ----> Animal.prototype ----> Function.prototype ----> Object.prototype

构造继承就是在子类中执行父类构造函数中的逻辑, 将父类的属性添加到子类实例的this身上,再结合上自己的属性一起返回。

特点:实现简单、可以向父类传参、原型互不影响

缺点:没有继承父类原型中的属性

3.组合继承

所谓组合继承,可以理解为将上面的两种方法组合,通过修改原型链继承原型中的属性,通过绑定构造函数继承父类构造函数中的属性。

function Animal(type) {
	this.type = type
}
Animal.prototype.sayHi = function() { console.log("Hi", this.name) }

function Cat(name) {
  Animal.call(this,"Cat") 
	this.name = name
}
Cat.prototype = Animal.prototype //直接继承父类原型, 可以省去实例化对象的过程
Cat.prototype.constructor = Cat //手动修改原型链关系

var cat = new Cat("小猫")
cat.type // Cat
cat.name //小猫
cat.sayHi() //Hi 小猫
//cat.__proto__ ----> Animal.prototype ----> Function.prototype ----> Object.prototype
//animal.__proto__ ----> Animal.prototype ----> Function.prototype ----> Object.prototype

特点:可以向父类传参,也继承了父类原型中的属性,但是父子类还是共用同一原型

缺点: 1. 二者原型指向同一个引用,修改原型中的属性会彼此影响

  1. 需要手动维护子类原型链关系

上面子类原型直接继承的是父类的原型对象,如果改成上面原型继承中的直接继承父类实例对象会不会又啥问题呢?

Cat.prototype = new Animal("Cat")
//首先Animal函数会调用两次,一次作为普通函数调用, 一次作为构造函数实例化调用
//实例的属性原型的属性有部分重复
cat // {type: "Cat", name: "小猫"}
cat.__proto__ // {type: "Cat", constructor: ƒ}
cat.__proto__.__proto__ //{sayHi: ƒ, constructor: ƒ}

4.寄生组合继承

如果直接通过修改原型链实现继承的话,总会出现上述的原型共用问题;在组合继承方式的基础上,利用空函数作为中介

function Animal(type) {
	this.type = type
}
Animal.prototype.sayHi = function() { console.log("Hi", this.name) }

function Cat(name) {
  Animal.call(this,"Cat") 
	this.name = name
}
function F() {}
F.prototype = Animal.prototype
Cat.prototype = new F()
var cat = new Cat("小猫")

cat //  {type: "Cat", name: "小猫"} 
cat.__proto__ // {}
cat.__proto__.__proto__ // {sayHi: ƒ, constructor: ƒ}

//cat.__proto__ ----> new F() ----> Animal.prototype ----> Function.prototype ----> Object.prototype
//animal.__proto__ ----> Animal.prototype ----> Function.prototype ----> Object.prototype

在继承完成之后,父类构造函数中type成了子类实例的自有属性, 父类原型中的属性sayHi成了子类实例原型链中的属性, 这其实与ES6中通过extends实现继承的效果一致。

5.拷贝继承

本来继承的目的也是为了让子类拥有父类的属性,如果把父类的所有属性和方法拷贝到子类中,那还不也是一种办法么

//对象的深度拷贝
function extend(child, parent) { //这里child, parent都是对象, 如果是构造函数的话,就要取构造函数的原型prototype
  for (var k in parent) {
    if(typeof parent[k] === 'object') { // 对象或数组
      child[k] = parent[k].constructor === Array ? [] : {}
      extend(child[k], parent[k])
    } else {
      child[k] = parent[k]
    }
  }
  return child
}

ES6中的继承

ES6中使用extends关键字实现继承

class Animal{
  constructor(type) {
    this.type = type
  }
  sayHi () {
    console.log("Hello Animal")
  }
}

class Cat extends Animal{
  constructor(name) {
    super() //子类构造函数中必须先调用super()
    this.name = name
  }
}

var cat = new Cat("小猫")
cat //  {type: "Cat", name: "小猫"} 
cat.__proto__ // {}
cat.__proto__.__proto__ // {sayHi: ƒ, constructor: ƒ}
//与ES5中寄生组合继承的效果一致