学习JavaScript中类,原型和继承

0xGeekCat · 2020-8-24 · 次阅读


构造函数和对象

在学习之前先了解一下其他语言是如何定义类的

在JAVA中类是创建对象的模板,👇定义一个Person

public class Person{
  String name;
  int age;
  String gender;
  void wake(){}
  void eat(){}
  void sleep(){}
}

但是对于JavaScript,在ECMAScript 6之前并没有class语法;在JavaScript中通过构造函数[等效其他语言的类]来创建对象

构造函数是一个提供生成对象的模板并描述对象基本结构的函数。一个构造函数可以生成多个对象,每个对象都有相同的结构

🔔构造函数就是对象的模板,对象就是构造函数的实例

function Person(name) {
    this.name = name

    this.sayName = function () {
        return this.name
    }
}

let person1 = new Person('hacker')
let person2 = new Person('geeker')

console.log(person1.constructor === Person) 👈 true

通过new构建实例化对象,构造函数中的this总是指向实例化的对象,每个实例对象都有一个不可枚举的属性constructor属性来指向构造函数,即Person

截屏2020-08-24 下午8.22.22

实例对象中可以看到Person类中定义的name属性和sayName方法

❓没有constructor属性;但也能取到其值为Person;此原因稍后解释

截屏2020-08-24 下午8.25.44

构造函数缺点

所有实例对象都单独创建属性和方法;也就是说,每实例化一个对象👆namesayName就会创建一次;其实际是绑定在对象上,而不是绑定在构造函数中;但是在有的场景中属性或者方法需要时通用的

function Person(name) {
    this.name = name

    this.sayName = function () {
        return this.name
    }

    this.sleep = function () {
        console.log('sleep')
    }
}

let person1 = new Person('hacker')
let person2 = new Person('geeker')

console.log(person1.sleep === person2.sleep) 👈 false

prototype原型对象

为了解决👆实例对象之间共享属性和方法问题,JavaScript提供了prototype属性

function Person(name) {
    this.name = name

    this.sayName = function () {
        return this.name
    }
}

Person.prototype.sleep = function () {
    console.log('sleep')
}

let person1 = new Person('hacker')
let person2 = new Person('geeker')

console.log(person1.sleep === person2.sleep) 👈 true

构造函数通过prototype指向原型对象,即构造函数才有prototype属性,其作用是让构造函数创建的所有实例对象都能找到公用的属性和方法

🔔实际上任何构造函数在创建实例对象的时候,都会关联其自身的prototype对象

截屏2020-08-24 下午9.10.51

❗️可以修改原型对象的引用,但是仍需要把constructor属性指向构造函数

👇对上个例子中sleep方法进行改写

Person.prototype = {
    constructor: Person,
    sleep: function () {
        console.log('sleep')
    }
}

原型链

根据👆所学知识作为基础可以知道,实例对象的属性和方法可以定义在自身,也可以定义在原型对象上

通过上面的对象执行sleep方法不难发现,实例对象能够直接获取原型对象上的属性和方法

在之前打印person1时,仔细观察不难发现有个特别的属性__proto__,其指向实例对象的原型对象

截屏2020-08-24 下午9.33.35

当访问对象上没有的所要查询的属性时,就会去__proto__中查询;如果还是查询不到,就会继续查询原型对象的__proto__,直到所查询的原型对象为null

🔔可以通过__proto__构成了一条原型链

截屏2020-08-24 下午9.39.56

观察Person构造函数的原型对象可以发现其含有constructor属性;这点解答了👆关于实例对象为什么没有constructor属性但还是可以执行的问题,**constructor属性实际上存在于原型对象**,在刚刚改写sleep方法时也不难发现这点

👇虚线表示该属性或方法并不是真正存在,person1.constructor是通过__proto__间接实现的

截屏2020-08-24 下午9.54.41

原型对象显然也是一个对象,既然是对象那么肯定也有自己的原型对象;在JavaScript中所有的对象都是Object的实例,并继承Object.prototype的属性和方法let a = {}实际上是new Object()的语法糖

❗️解释说明:语法糖指的是计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。语法糖让程序更加简洁,有更高的可读性

let obj1 = {}
let obj2 = new Object()

console.log(obj1.__proto__ === Object.prototype) 👈 true
console.log(obj2.__proto__ === Object.prototype) 👈 true
console.log(person1.__proto__.__proto__ === Object.prototype) 👈 true

截屏2020-08-24 下午10.08.20

constructor属性

原型对象的constructor用来指向构造函数

console.log(Person.prototype.constructor === Person) 👈 true

constructor还可以用来判数据类型

console.log((2).constructor === Number); 👈 true
console.log((true).constructor === Boolean); 👈 true
console.log(('str').constructor === String); 👈 true
console.log(([]).constructor === Array); 👈 true
console.log((function() {}).constructor === Function); 👈 true
console.log(({}).constructor === Object); 👈 true

但是一旦更改原型对象,这种判断类型的方式就不可靠

function func() {}
func.prototype = []
f = new func()

console.log(f.constructor === Function) 👈 false
console.log(f.constructor === Array) 👈 true

JavaScript中构造函数本身也可以看成是对象,这种即是函数也是对象的称为函数对象;调用构造函数的f.callf.apply其实调用的是继承自其原型对象的Function.prototype.callFunction.prototype.apply,因此构造函数都是Function函数的实例对象;既然是实例对象,构造函数也拥有__proto__constructor属性

截屏2020-08-25 上午9.08.58

🔔Person构造函数和JavaScript的普通对象没有任何区别,有constructor属性指向Function函数,说明Person函数是Function函数的实例对象;而且constructor属性不在Person构造函数上而在其原型对象Function.prototype

截屏2020-08-25 上午9.12.11

鸡生蛋蛋生鸡

发现最终👆的原型图指向四个对象:ObjectObject.prototypeFunctionFunction.prototype,其之间的关系在整个原型关系里最难理解

截屏2020-08-25 上午9.15.02

Object函数和Person函数一样都是函数对象,因此都是Function函数的实例对象

截屏2020-08-25 上午9.38.03

👇Object和Function的关系

截屏2020-08-25 上午9.39.02

Object是构造函数,则Function也能通过new Function() {}来构造匿名函数,Function同时也是自己的constructor

截屏2020-08-25 下午2.17.59

Function.prototype同样也是实例对象,因此它的原型对象自然是Object.prototype

截屏2020-08-25 下午2.21.48

🔔ObjectObject.prototypeFunctionFunction.prototype四者间关系

截屏2020-08-25 下午2.22.17

Object继承自Function.prototypeFunction.prototype继承自Object.prototype

❓那么到底是先有Object,还是先有Function

这个鸡和蛋问题的根本原因在于Function.__proto__指向Function.prototype,让Function间接继承了Object.prototype

截屏2020-08-25 下午3.06.43

❗️关于Function.prototype

  • 和普通函数一样可以调用,但总是返回undefined
  • 继承于Object.prototype,且没有prototype属性

由于Object.prototype.__proto__ === null,所以原型链溯源到Object.prototype就结束了

🔔先有 Object.prototype(原型链顶端),Function.prototype 继承自 Object.prototype ;最后FunctionObject以及其它构造函数继承Function.prototype而产生

截屏2020-08-25 下午3.17.46

静态属性和方法

静态方法是指不需要声明类的实例就可以直接使用的方法。在JAVA中通过static定义静态方法

public class Person{
  static int num = 0;
  public static void say() {
    System.out.println("hello"); 
  }
  public static void main(String[] args){
    Person.say();
  }
}

ES5中,直接通过设置构造函数属性的方式创建静态属性和方法

function Person() {}

Person.sleep = function () {
    console.log('sleep')
    console.log(this) 👈 [Function: Person] { sleep: [Function], num: 10 }
}
Person.num = 10

console.log(Person.num)
Person.sleep()

静态方法和实例方法最主要的区别就是实例方法可以访问到实例对象,可以对实例进行操作,而静态方法一般用于跟实例无关的操作。静态方法最常见的是在jQuery的一些工具函数中,比如ajax()和trim()

ES5继承

继承就是指子类继承父类所有的属性和方法;同时要知道此处父类指的不仅仅是自身的构造函数,原型链上还会有父类的父类等,也需要一同继承

原型链继承

function Parent () {
    this.name = 'Mac';
    this.sayName = function() {
        return this.name
    }
}
Parent.prototype.sleep = function () {
    console.log('sleep')
}

function Child () {}
Child.prototype = new Parent();

let child1 = new Child();
console.log(child1.sayName()) 👈 Mac
child1.sleep() 👈 sleep

👆将父类的实例挂载到子类原型上,那么所有的子类就能访问到父类的属性和方法

❓但所有子类共享原型对象也会带来一些问题

  • 父类属性被某个子类篡改,其余所有子类属性都将一同改变

    function Parent () {
        this.name = ['Mac', 'Mary'];
    }
    
    function Child() {}
    Child.prototype = new Parent()
    
    let child1 = new Child()
    child1.name.push('A')
    
    let child2 = new Child()
    console.log(child2.name) 👈 [ 'Mac', 'Mary', 'A' ]
  • 创建子类实例的时候,不能向父类传参数

  • 不能继承静态方法

构造函数继承

function Parent(name) {
    this.name = name
    this.color = ['red','yellow']
}
Parent.prototype.sleep = function(){
    console.log('sleep')
}

function Child(name) {
    Parent.call(this, name)
}

let child1 = new Child('Mac')
child1.color.push('blue')

let child2 = new Child('Mary')
console.log(child2.color) 👈 [ 'red', 'yellow' ]
console.log(child2.sleep) 👈 undefined

创建子类实例时调用父类构造函数,避免属性被其他子类篡改;此时也可以向父类传参数,但是还是没有继承父类原型上的属性和方法

组合继承

function Parent(name) {
    this.name = name
    this.color = ['red','yellow']
}
Parent.prototype.sleep = function(){
    console.log('sleep')
}

function Child(name) {
    Parent.call(this, name)
}
Child.prototype = new Parent()

let child1 = new Child('Mac')
child1.color.push('blue')

let child2 = new Child('Mary')
console.log(child2.color) 👈 [ 'red', 'yellow' ]
console.log(child2.sleep) 👈 sleep

融合原型链继承和构造函数继承的优点,是JavaScript中常用的继承方式

ES6继承

ES6新增了class关键词用来定义一个类;但本质上只是ES5构造函数的语法糖,大多部分功能ES5都能实现

class Person{
    constructor(name){
        this.name = name
    }
    sayName(){
        return this.name
    }
    static sleep() {
        console.log('sleep');
    }
}
Person.prototype.hack = function(){
    console.log('hack')
}

Person.sleep()
let person = new Person('Mac')
person.hack()
console.log(person.sayName())

ES6的继承可以通过extends关键词实现,比ES5修改原型链来实现继承要更清晰和方便

class Person{
    constructor(name){
        this.name = name
    }
    sayName(){
        return this.name
    }
    static sleep() {
        console.log('sleep');
    }
}
Person.prototype.hack = function(){
    console.log('hack')
}

class Child extends Person{
    constructor(name,color){
        super(name)
        this.color = color
    }
    sayColor(){
        return 'my color is:'+ this.color
    }
}
let child = new Child('child', 'yellow')
console.log(child.sayName())
console.log(child.sayColor())
child.hack()
console.log(child.sleep) 👈 undefined JavaScript中静态方法不会被继承

reference

一文读懂JS中类、原型和继承

郑重感谢