深入JS面向对象

66 阅读29分钟

面向对象是现实的抽象方式

对象是JavaScript中一个非常重要的概念,这是因为对象可以将多个相关联的数据封装到一起,更好的描述一个事物。

比如我们可以描述一辆车:Car,具有颜色(color)、速度(speed)、品牌(brand)、价格(price),行驶(travel)等等;比如我们可以描述一个人:Person,具有姓名(name)、年龄(age)、身高(height),吃东西(eat)、跑步(run)等等;

用对象来描述事物,更有利于我们将现实的事物,抽离成代码中某个数据结构,所以有一些编程语言就是纯面向对象的编程语言,比Java,你在实现任何现实抽象时都需要先创建一个类,根据类再去创建对象。

JavaScript的面向对象

JavaScript其实支持多种编程范式的,包括函数式编程和面向对象编程:

JavaScript中的对象被设计成一组属性的无序集合,像是一个哈希表,有key和value组成;key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型;如果值是一个函数,那么我们可以称之为是对象的方法;

如何创建一个对象呢?

早期使用创建对象的方式最多的是使用Object类,并且使用new关键字来创建一个对象,这是因为早期很多JavaScript开发者是从Java过来的,它们也更习惯于Java中通过new的方式创建一个对象;后来很多开发者为了方便起见,都是直接通过字面量的形式来创建对象:这种形式看起来更加的简洁,并且对象和属性之间的内聚性也更强,所以这种方式后来就流行了起来。

创建对象的两种方式

对属性操作的控制

在前面我们的属性都是直接定义在对象内部,或者直接添加到对象内部的,但是这样来做的时候我们就不能对这个属性进行一些限制:比如这个属性是否是可以通过delete删除的?这个属性是否在for-in遍历的时候被遍历出来呢?

如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符。通过属性描述符可以精准的添加或修改对象的属性;属性描述符需要使用 Object.defineProperty 来对属性进行添加或者修改;

Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。

可接收三个参数:

obj要定义属性的对象;
prop要定义或修改的属性的名称或 Symbol;
descriptor要定义或修改的属性描述符,是个对象。

返回值:

被传递给函数的对象。

属性描述符分类

属性描述符的类型有两种:

数据属性(Data Properties)描述符(Descriptor);
存取属性(Accessor访问器 Properties)描述符(Descriptor);

数据属性描述符

数据属性描述符有如下四个值:

  1. [[Configurable]]:设置为false表示属性不可删除,也不可重新定义属性描述符
  2. [[Enumerable]]:设置为false表示属性不可通过for-in或者Object.keys()遍历的到该属性;
  3. [[Writable]]:设置为false表示不可修改属性的值;
  4. [[value]]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改;
  • 当我们直接在一个对象上定义某个属性时,这个属性的Configurable、Enumerable、Writable默认是true、value默认是我们赋值的value。
  • 当我们通过属性描述符定义一个属性时,这个属性的Configurable、Enumerable、Writable默认是false、value默认是undefined。

数据属性描述符测试代码

存取属性描述符

存取属性描述符有如下四个值:

  1. [[Configurable]]:设置为false表示属性不可删除,也不可重新定义属性描述符
  2. [[Enumerable]]:设置为false表示属性不可通过for-in或者Object.keys()遍历的到该属性;
  3. [[get]]:获取属性时会执行的函数。默认为undefined
  4. [[set]]:设置属性时会执行的函数。默认为undefined

存取属性描述符测试代码

var obj = {
  name: "why",
  age: 18,
  _address: "北京市"
}

// 存取属性描述符
// 1.隐藏某一个私有属性不希望直接被外界使用和赋值
// 2.如果我们希望截获某一个属性它访问和设置值的过程时, 也会使用存储属性描述符
Object.defineProperty(obj, "address", {
  enumerable: true,
  configurable: true,
  get: function() {
    console.log("获取了一次address的值")
    return this._address
  },
  set: function(value) {
    console.log("设置了addres的值")
    this._address = value
  }
})

console.log(obj.address)

obj.address = "上海市"
console.log(obj.address)

存取属性描述符使用场景

  1. 隐藏某一个私有属性不希望直接被外界使用和赋值
  2. 如果我们希望截获某一个属性它访问和设置值的过程时, 也会使用存储属性描述符

同时定义多个属性

Object.defineProperties()(注意多了个s) 方法直接在一个对象上定义多个新的属性或修改现有属性,并且返回该对象。

var obj = {
  // 私有属性(js里面是没有严格意义的私有属性)
  _age: 18,
  _eating: function() {},
}

Object.defineProperties(obj, {
  name: {
    configurable: true,
    enumerable: true,
    writable: true,
    value: "why"
  },
  age: {
    configurable: true,
    enumerable: true,
    get: function() {
      return this._age
    },
    set: function(value) {
      this._age = value
    }
  }
})

obj.age = 19
console.log(obj.age)
console.log(obj)

对象方法补充

  1. Object.getOwnPropertyDescriptor()获取对象的属性描述符
Object.getOwnPropertyDescriptor(obj, 'name') 获取对象中某个属性的属性描述符
Object.getOwnPropertyDescriptors(obj) 获取对象中所有的属性描述符

2. Object.preventExtensions()禁止对象扩展新属性

Object.preventExtensions(obj)
给一个对象添加新的属性会失败(在严格模式下会报错,非严格模式下会失败)

3. Object.seal()密封对象,不允许配置和删除属性()

Object.seal(obj)
实际上是将现有属性的configurable: false

4. Object.freeze()冻结对象,不允许修改现有属性

Object.freeze(obj)
实际上是将现有属性的writable: false

创建多个对象的方案

如果我们现在希望创建一系列的对象:比如Person对象,包括张三、李四、王五、李雷等等,他们的信息各不相同;那么采用什么方式来创建比较好呢?

目前我们已经学习了两种方式:

  • new Object方式;
  • 字面量创建的方式;

这种方式有一个很大的弊端:创建同样的对象时,需要编写重复的代码;

创建对象的方案 – 工厂函数

我们可以想到的一种创建对象的方式:工厂模式,工厂模式其实是一种常见的设计模式;通常我们会有一个工厂方法,通过该工厂方法我们可以产生想要的对象;

// 工厂模式: 工厂函数
function createPerson(name, age, height, address) {
  var p = {}  // 获取使用new Object()
  p.name = name
  p.age = age
  p.height = height;
  p.address = address

  p.eating = function() {
    console.log(this.name + "在吃东西~")
  }

  p.running = function() {
    console.log(this.name + "在跑步~")
  }

  return p
}

var p1 = createPerson("张三", 18, 1.88, "广州市")
var p2 = createPerson("李四", 20, 1.98, "上海市")
var p3 = createPerson("王五", 30, 1.78, "北京市")

// 工厂模式的缺点(获取不到对象最真实的类型)
console.log(p1, p2, p3)

工厂函数创建对象的时候,不管我们使用{}还是new Object()有一个比较大的问题就是:我们在打印对象时,获取不到对象最真实的类型。但是从某些角度来说,这些对象应该有一个他们单独的类型。

下面我们来看一下另外一种模式:构造函数的方式;我们先理解什么是构造函数?

认识构造函数

构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数。在其他面向的编程语言里面,构造函数是存在于类中的一个方法,称之为构造方法,但是JavaScript中的构造函数有点不太一样。

JavaScript中的构造函数是什么样的?

构造函数也是一个普通的函数,从表现形式来说,和千千万万个普通的函数没有任何区别。如果这么一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数

那么被new调用有什么特殊的呢?

new操作符调用的作用

如果一个函数被使用new操作符调用了,那么它会执行如下操作:

  1. 内存中创建一个新的对象;
  2. this指向创建出来的新对象;
  3. 执行函数体代码
  4. 返回创建出来的新对象;
  5. 这个对象内部的__proto__属性会被赋值为该构造函数的prototype属性;

创建对象的方案 – 构造函数

我们来通过构造函数实现一下:

// 规范: 构造函数的首字母一般是大写
function Person(name, age, height, address) {
  this.name = name
  this.age = age
  this.height = height
  this.address = address

  this.eating = function() {
    console.log(this.name + "在吃东西~")
  }

  this.running = function() {
    console.log(this.name + "在跑步")
  }
}


var p1 = new Person("张三", 18, 1.88, "广州市")
var p2 = new Person("李四", 20, 1.98, "北京市")

console.log(p1)
console.log(p2)
p1.eating()
p2.eating()

console.log(p1.eating === p2.eating)  //false
console.log(p1.running === p2.running) //false 

这个构造函数可以确保我们的对象是Person的类型的(实际是constructor的属性,这个我们后续再探讨);

但是构造函数就没有缺点了吗?

构造函数也是有缺点的,它在于我们需要为每个对象的函数去创建一个函数对象实例;不信你可以看,上面的打印都是false。对于对象的属性,每个对象的属性值都不一样,占用不同的内存是没事的,但是对于函数,如果还占用不同的内存,就显得没必要了。我们可以将这些函数放到Person.prototype对象上即可。

认识对象原型

JavaScript当中每个对象都有一个特殊的内置属性 [[prototype]],这个特殊的对象可以指向另外一个对象。

那么这个对象有什么用呢?

当我们通过引用对象的属性key来获取一个value时,它会触发 [[Get]]的操作;这个操作会首先检查该属性是否有对应的属性,如果有的话就使用它;如果对象中没有改属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;

那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?

答案是有的,只要是对象都会有这样的一个内置属性;获取的方式有两种:

  • 方式一:通过对象的 __proto__ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);
  • 方式二:通过 Object.getPrototypeOf(obj) 方法可以获取到(ES5提供的);

这个__proto__我们也称为隐式原型。

函数的原型 prototype

那么我们知道上面的东西对于我们的构造函数创建对象来说有什么用呢?它的意义是非常重大的,接下来我们继续来探讨;

函数作为一个对象也有一个__proto__,但是函数作为一个函数还有一个显式原型,也就是prototype的属性,所有的函数都有一个prototype的属性。

你可能会问题,老师,是不是因为函数是一个对象,所以它有prototype的属性呢?

不是的,因为它是一个函数,才有了这个特殊的属性;而不是它是一个对象,所以有这个特殊的属性;

再看new操作符

我们前面讲过new关键字的步骤如下:

  1. 在内存中创建一个新的对象;
  2. 这个对象内部的__proto__属性会被赋值为该构造函数的prototype属性;

那么也就意味着我们通过Person构造函数创建出来的所有对象的[[prototype]]属性都指向Person.prototype。

创建对象的内存表现

赋值为新的对象

prototype添加属性

constructor属性

事实上原型对象上面是有一个属性的:constructor,默认情况下原型上都会添加一个属性叫做constructor,这个constructor指向当前的函数对象。

重写原型对象

如果我们需要在原型上添加过多的属性,通常我们会重新整个原型对象:

前面我们说过,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获取constructor属性。

而我们这里相当于给prototype重新赋值了一个对象,那么这个新对象的constructor属性,会指向Object构造函数,而不是Person构造函数了。

原型对象的constructor

如果希望constructor指向Person,那么可以手动添加:constructor: Person

上面的方式虽然可以, 但是也会造成constructor的[[Enumerable]]特性被设置了true, 原来的是false。

默认情况下, 原生的constructor属性是不可枚举的, 如果希望解决这个问题, 我们可以不用constructor: Person,可以使用我们前面介绍的Object.defineProperty()函数。

创建对象 – 构造函数和原型组合

我们在上一个构造函数的方式创建对象时,有一个弊端:会创建出重复的函数,比如running、eating这些函数,那么有没有办法让所有的对象去共享这些函数呢?

可以,将这些函数放到Person.prototype的对象上即可。

JavaScript中的类和对象

当我们编写如下代码的时候,我们会如何来称呼这个Person呢?

在JS中Person应该被称之为是一个构造函数;从很多面向对象语言过来的开发者,也习惯称之为类,因为类可以帮助我们创建出来对象p1、p2;如果从面向对象的编程范式角度来看,Person确实是可以称之为类的;

面向对象的特性 – 继承

面向对象有三大特性:封装、继承、多态

  • 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
  • 继承:继承是面向对象中非常重要的,不仅仅可以减少重复代码的数量,也是多态前提(纯面向对象中);
  • 多态:不同的对象在执行时表现出不同的形态;

这里我们核心讲继承。那么继承是做什么呢?继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。

那么JavaScript当中如何实现继承呢?

不着急,我们先来看一下JavaScript原型链的机制,再利用原型链的机制实现一下继承。

JavaScript原型链

在真正实现继承之前,我们先来理解一个非常重要的概念:原型链。我们知道,从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型上面获取。

Object的原型

那么什么地方是原型链的尽头呢?比如第三个对象是否也是有原型__proto__属性呢?

我们会发现它打印的是 [Object: null prototype] {},事实上这个原型就是我们最顶层的原型了,从Object直接创建出来的对象的原型都是 [Object: null prototype] {}。

那么我们可能会问题: [Object: null prototype] {} 原型有什么特殊吗?

  • 特殊一:该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了;
  • 特殊二:这个顶层的原型虽然打印的时候是空的,但是它不是空的,只是它的属性都是不可枚举的, 该对象上有很多默认的属性和方法;

创建Object对象的内存图

原型链关系的内存图

Object是所有类的父类

从我们上面的Object原型我们可以得出一个结论:原型链最顶层的原型对象就是Object的原型对象。

① 通过原型链继承

如果我们现在需要实现继承,那么就可以利用原型链来实现了,目前stu的原型是p对象,而p对象的原型是Person默认的原型,里面包含running等函数;注意:步骤4和步骤5不可以调整顺序,否则会有问题。

// 父类: 公共属性和方法
function Person() {
  this.name = "why"
  this.friends = [] //引用的属性
}

Person.prototype.eating = function() {
  console.log(this.name + " eating~")
}

// 子类: 特有属性和方法
function Student() {
  this.sno = 111
}

var p = new Person()
Student.prototype = p

Student.prototype.studying = function() {
  console.log(this.name + " studying~")
}

// name/sno
var stu = new Student()

console.log(stu.name) //why
stu.eating()
stu.studying()

上面代码,父类有name属性和eating方法,子类有sno属性和studying方法,这是最简单的继承,关系图如下:

但是这种方式有弊端:

    // 1.第一个弊端: 打印stu对象, 继承的属性是看不到的
    console.log(stu.name) // 只会打印sno,打印不出来name

    // 2.第二个弊端: 引用的属性会相互影响
    // 创建出来两个stu的对象
    var stu1 = new Student()
    var stu2 = new Student()
    // 获取引用, 修改引用中的值, 会相互影响
    stu1.friends.push("kobe")
    console.log(stu1.friends) // ['kobe']
    console.log(stu2.friends) // ['kobe']
    
    // 但是直接修改对象上的属性,不会影响,因为是js引擎优先给本对象添加了一个新属性
    stu1.name = "kobe"
    console.log(stu2.name) //why

    // 3.第三个弊端: 在前面实现类的过程中都没有传递参数
    var stu3 = new Student("lilei", 112)

原型链继承的弊端

但是目前有一个很大的弊端:某些属性其实是保存在p对象上的;

  • 第一,我们通过直接打印对象是看不到这个属性的;
  • 第二,这个属性会被多个对象共享,如果这个对象是一个引用类型,那么就会造成问题;
  • 第三,不能给Person传递参数,因为这个对象是一次性创建的(没办法定制化);

② 借用构造函数继承(经典继承)

为了解决原型链继承中存在的问题,开发人员提供了一种新的技术: constructor stealing(有很多名称: 借用构造函数或者称之为经典继承或者称之为伪造对象):

steal是偷窃、剽窃的意思,但是这里可以翻译成借用;

借用继承的做法非常简单:在子类型构造函数的内部调用父类型构造函数.

因为函数可以在任意的时刻被调用;因此通过apply()和call()方法也可以在新创建的对象上执行构造函数;

    // 父类: 公共属性和方法
    function Person(name, age, friends) {
      // this = stu
      this.name = name
      this.age = age
      this.friends = friends
    }

    Person.prototype.eating = function() {
      console.log(this.name + " eating~")
    }

    // 子类: 特有属性和方法
    function Student(name, age, friends, sno) {
      // 调用父类的构造函数,并绑定this为子类
      Person.call(this, name, age, friends)
      // this.name = name
      // this.age = age
      // this.friends = friends
      this.sno = 111
    }

    var p = new Person()
    Student.prototype = p

    Student.prototype.studying = function() {
      console.log(this.name + " studying~")
    }

    // name/sno
    var stu = new Student("why", 18, ["kobe"], 111)
    // console.log(stu.name)
    // stu.eating()
    // stu.studying()

内存图如下:

借用构造函数继承可以解决上面的三个弊端:

  1. 继承的属性打印不到,这时候属性都在子类上了,肯定可以打印的到
  2. 引用属性的问题,这时候子类对象都有独立的属性,所以也不会相互影响
  3. 参数传递也可以了

但是借用构造函数也是有弊端

  1. 第一个弊端: Person函数至少被调用了两次
  • Person.call(this, name, age, friends) 调用了一次
  • var p = new Person() 调用了一次
  1. 第二个弊端: stu的原型对象上,也就是上面的p对象也会多出来一些属性(虽然他们是underfined),但是这些属性是没有存在的必要

其实这时候一个子类实例都有两份父类的属性的,一份是在子类实例上,一份是在子类的原型对象也就是p上(当然都是underfined),这两份属性我们无需担心访问出现问题,因为默认一定是访问实例本身这一部分的。所以这种方式也不是最完美的。

其实上面两个弊端的根本原因是因为把var p = new Person()作为了stu的原型对象。 可能你会说,直接把父类的原型赋值给子类的原型不就行了吗?如下:

// 直接将父类的原型赋值给子类, 作为子类的原型
Student.prototype = Person.prototype

这样是更不行的,因为这时候子类的原型和父类的原型都是同一个对象了,当我们给子类的原型上加一个方法,那么父类的原型上也有这个方法了,这肯定是不行的:

Student.prototype.studying = function() {
   console.log(this.name + " studying~")
}

上面方式弊端的根本原因就是stu的原型是通过new Person()创建的,那么我们能不能不使用new Person()而是使用其他对象作为stu的原型呢?

引入原型式继承函数

这种模式要从道格拉斯·克罗克福德(Douglas Crockford,著名的前端大师,JSON的创立者)在2006年写的一篇文章说起: Prototypal Inheritance in JavaScript(在JS中使用原型式继承),在这篇文章中,它介绍了一种继承方法,而且这种继承方法不是通过构造函数来实现的.

// 作为原型对象的对象
var person = {
  name: "why",
  age: 18
}

// 原型式继承函数
// 传进来原型对象
function createObject1(obj) {
  var newObj = {}
  // 将newObj的原型设置为传进来的对象
  Object.setPrototypeOf(newObj, obj)
  return newObj
}

// 但是当道格拉斯·克罗克福德提出这种方式的时候还没有setPrototypeOf函数,他是通过如下方法实现的:
function createObject2(obj) {
  function Fn() {}
  Fn.prototype = obj
  var newObj = new Fn()
  return newObj
}

// var stu = createObject2(person)

// 但是上面两种方式我们都不用,因为我们使用后Object.create(obj)和上面两种方式本质是一样的
// 都是将obj作为新创建出来对象的原型
var stu = Object.create(person)
console.log(info)
console.log(info.__proto__)

最终的效果:stu对象的原型指向了person对象;但是这种方式只是对象的继承,而不是我们希望的构造函数的继承。

寄生式继承(可忽略)

寄生式(Parasitic)继承:

寄生式(Parasitic)继承是与原型式继承紧密相关的一种思想, 并且同样由道格拉斯·克罗克福德(DouglasCrockford)提出和推广的,寄生式继承的思路是结合原型类继承和工厂模式的一种方式,即创建一个封装继承过程的函数, 该函数在内部以某种方式来增强对象,最后再将这个对象返回。

var personObj = {
  running: function() {
    console.log("running")
  }
}

function createStudent(name) {
  var stu = Object.create(personObj)
  stu.name = name
  stu.studying = function() {
    console.log("studying~")
  }
  return stu
}

var stuObj = createStudent("why")
var stuObj1 = createStudent("kobe")
var stuObj2 = createStudent("james")

这时候createStudent就相当于一个工厂函数。但是这种方式也有弊端,就比如上面的每个stu都有一个重复的studying方法。而且工厂函数我们也不知道创建出来对象的类型(这就回到了工厂函数的弊端了)。

③ 寄生组合式继承(最终方案)

现在我们来回顾一下之前提出的比较理想的组合继承,组合继承是比较理想的继承方式, 但是存在两个问题:

  • 问题一: 构造函数会被调用两次: 一次在创建子类型原型对象的时候, 一次在创建子类型实例的时候.
  • 问题二: 父类型中的属性会有两份: 一份在原型对象中, 一份在子类型实例中.

事实上, 我们现在可以利用寄生式继承将这两个问题给解决掉.

你需要先明确一点: 当我们在子类型的构造函数中调用父类型.call(this, 参数)这个函数的时候, 就会将父类型中的属性和方法复制一份到了子类型中,所以父类型本身里面的内容, 我们不再需要,这个时候, 我们还需要获取到一份父类型的原型对象中的属性和方法。

能不能直接让子类型的原型对象 = 父类型的原型对象呢?

不要这么做, 因为这么做意味着以后修改了子类型原型对象的某个引用类型的时候, 父类型原生对象的引用类型也会被修改。

我们使用前面的寄生式思想就可以了。

// 当然下面的Objec.create(SuperType.prototype)我们也可以使用createObject实现,都是一样的
// 这两个方法就是寄生组合式继承的核心方法
function createObject(o) {
  function Fn() {}
  Fn.prototype = o
  return new Fn()
}

function inheritPrototype(SubType, SuperType) {
  // 创建一个新对象作为子类的原型,并且将新对象的原型指向SuperType.prototype
  SubType.prototype = Objec.create(SuperType.prototype)
  //一般我们打印对象是Person{}这种方式的,其中Person就是constructor.name
  //因为我们上面是重新给SubType.prototype赋值给其他对象的,这时候子类的原型上是没有constructor的
  //所以当打印constructor的时候,先从子类原型上找,找不到,就去父类原型上找,父类原型上有Person,所以会打印Person{}
  // 所以我们需要给子类原型上添加constructor属性,如下,这时候打印子类对象就是Student{}了
  Object.defineProperty(SubType.prototype, "constructor", {
    enumerable: false,
    configurable: true,
    writable: true,
    value: SubType
  })
}

function Person(name, age, friends) {
  this.name = name
  this.age = age
  this.friends = friends
}

Person.prototype.running = function() {
  console.log("running~")
}

Person.prototype.eating = function() {
  console.log("eating~")
}


function Student(name, age, friends, sno, score) {
  Person.call(this, name, age, friends)
  this.sno = sno
  this.score = score
}

inheritPrototype(Student, Person)

Student.prototype.studying = function() {
  console.log("studying~")
}

var stu = new Student("why", 18, ["kobe"], 111, 100)
console.log(stu)
stu.studying()
stu.running()
stu.eating()

console.log(stu.constructor.name)

对象的方法补充

obj.hasOwnProperty('name'):对象是否有某一个属于自己的属性(不是在原型上的属性)
'name' in obj :判断某个属性是否在某个对象或者对象的原型上(不管在哪都算)
for (var key in obj) {} :不管在某个对象上还是它的原型上,都可以遍历出来
instanceof:用于检测构造函数的pototype,是否出现在某个实例对象的原型链上
console.log(stu instanceof Student) // true
console.log(stu instanceof Person) // true
console.log(stu instanceof Object) // true
isPrototypeOf:用于检测某个对象,是否出现在某个实例对象的原型链上
// 原型对象
var obj = {
  name: "why",
  age: 18
}
// 返回的对象
var info = Object.create(obj)
// 判断obj对象是否在info对象的原型链上
console.log(obj.isPrototypeOf(info)) //true

原型继承关系

左边的图一定要看懂:

认识class定义类

我们会发现,按照前面的构造函数形式创建类,不仅仅和编写普通的函数过于相似,而且代码并不容易理解。

在ES6(ECMAScript2015)新的标准中使用了class关键字来直接定义类;但是类本质上依然是前面所讲的构造函数、原型链的语法糖而已;所以学好了前面的构造函数、原型链更有利于我们理解类的概念和继承关系;

那么,如何使用class来定义一个类呢?

可以使用两种方式来声明类:类声明和类表达式;

类和构造函数的异同

我们来研究一下类的一些特性:你会发现它和我们的构造函数的特性其实是一致的;

类的构造函数

如果我们希望在创建对象的时候给类传递一些参数,这个时候应该如何做呢?

每个类都可以有一个自己的构造函数(方法),这个方法的名称是固定的constructor;当我们通过new操作符,操作一个类的时候会调用这个类的构造函数constructor;每个类只能有一个构造函数,如果包含多个构造函数,那么会抛出异常;

当我们通过new关键字操作类的时候,会调用这个constructor函数,并且执行如下操作:

  1. 在内存中创建一个新的对象;
  2. 这个对象内部的__proto__属性会被赋值为该类的prototype属性;
  3. 构造函数内部的this,会指向创建出来的新对象;
  4. 执行构造函数的内部代码;
  5. 返回创建出来的新对象;

类的实例方法

在上面我们定义的属性都是直接放到了this上,也就意味着它是放到了创建出来的新对象中,在前面我们说过对于实例的方法,我们是希望放到原型上的,这样可以被多个实例来共享,这个时候我们可以直接在类中定义。

类的访问器方法

我们之前讲对象的属性描述符时有讲过对象可以添加setter和getter函数的,那么类也是可以的:

类的静态方法

静态方法通常用于定义直接使用类来执行的方法,不需要有类的实例,使用static关键字来定义:

ES6类的继承 - extends

前面我们花了很大的篇幅讨论了在ES5中实现继承的方案,虽然最终实现了相对满意的继承机制,但是过程却依然是非常繁琐的。在ES6中新增了使用extends关键字,可以方便的帮助我们实现继承。

super关键字

我们会发现在上面的代码中我使用了一个super关键字,这个super关键字有不同的使用方式:

注意:在子(派生)类的构造函数中使用this或者返回默认对象之前,必须先通过super调用父类的构造函数!

super的使用位置有三个:子类的构造函数、实例方法、静态方法;

class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

  running() {
    console.log(this.name + " running~")
  }

  eating() {
    console.log(this.name + " eating~")
  }

  personMethod() {
    console.log("处理逻辑1")
    console.log("处理逻辑2")
    console.log("处理逻辑3")
  }

  static staticMethod() {
    console.log("PersonStaticMethod")
  }
}

// Student子类
class Student extends Person {
  //注意点1:在子类的构造方法中,必须先调用父类的构造方法,再使用this
  constructor(name, age, sno) {
    super(name, age)
    this.sno = sno
  }

  studying() {
    console.log(this.name + " studying~")
  }

  // 子类对父类的方法的重写
  running() {
    console.log("student " + this.name + " running")
  }

  // 注意点2:子类对父类的方法的重写,也可以通过super调用父类的逻辑
  personMethod() {
    // 复用父类中的处理逻辑
    super.personMethod()

    console.log("处理逻辑4")
    console.log("处理逻辑5")
    console.log("处理逻辑6")
  }

  // 子类重写父类的静态方法
  static staticMethod() {
    super.staticMethod()
    console.log("StudentStaticMethod")
  }
}

var stu = new Student("why", 18, 111)
console.log(stu)

继承内置类

我们也可以让我们的类继承自内置类,比如Array,从而达到增强内置类的目的。

class HYArray extends Array {
  firstItem() {
    return this[0]
  }

  lastItem() {
    return this[this.length-1]
  }
}

var arr = new HYArray(1, 2, 3)
console.log(arr.firstItem())
console.log(arr.lastItem())

混入效果(了解)

JavaScript的类只支持单继承:也就是只能有一个父类,那么在开发中我们我们需要在一个类中添加更多相似的功能时,应该如何来做呢?

这个时候我们可以使用混入,但是JS没有一个关键字来实现混入,我们只能通过其他方法达到混入的效果。

class Person {

}

function mixinRunner(BaseClass) {
  class NewClass extends BaseClass {
    running() {
      console.log("running~")
    }
  }
  return NewClass
}

function mixinEater(BaseClass) {
  return class extends BaseClass {
    eating() {
      console.log("eating~")
    }
  }
}

// 在JS中类只能有一个父类: 单继承
class Student extends Person {

}

var NewStudent = mixinEater(mixinRunner(Student))
var ns = new NewStudent()
ns.running()
ns.eating()

这种方式只能混入方法,对于属性还是无能为力的,作为了解就好了。

在react中的高阶组件

JavaScript中的多态

面向对象的三大特性:封装、继承、多态。

前面两个我们都已经详细解析过了,接下来我们讨论一下JavaScript的多态。

JavaScript有多态吗?

维基百科对多态的定义:多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口,或使用一个单一的符号来表示多个不同的类型。

非常的抽象,个人的总结:不同的数据类型进行同一个操作,表现出不同的行为,就是多态的体现

那么从上面的定义来看,JavaScript是一定存在多态的。

传统的面向对象多态

传统的面向对象多态是有三个前提:

  1. 必须有继承(是多态的前提)
  2. 必须有重写(子类重写父类的方法)
  3. 必须有父类指针指向子类对象
// Shape形状
class Shape {
  getArea() {}
}

class Rectangle extends Shape {
  getArea() {
    return 100
  }
}

class Circle extends Shape {
  getArea() {
    return 200
  }
}

var r = new Rectangle()
var c = new Circle()

// 多态: 当对不同的数据类型执行同一个操作时, 如果表现出来的行为(形态)不一样, 那么就是多态的体现.
function calcArea(shape: Shape) {
  console.log(shape.getArea())
}

calcArea(r)
calcArea(c)

export {}
JS中的多态

下面的代码,sum(m, n),当传入不同的数据类型进行同一个操作的时候,表现出来的结果也是不一样的,所以也是多态的体现,但是这里就没有传统多态的三个条件了。

// 这也是多态的体现
function sum(m, n) {
  return m + n
}

sum(20, 30)
sum("abc", "cba")
// 多态: 当对不同的数据类型执行同一个操作时, 如果表现出来的行为(形态)不一样, 那么就是多态的体现.
function calcArea(foo) {
  console.log(foo.getArea())
}

var obj1 = {
  name: "why",
  getArea: function() {
    return 1000
  }
}

class Person {
  getArea() {
    return 100
  }
}

var p = new Person()

calcArea(obj1)
calcArea(p)

字面量的增强

ES6中对 对象字面量 进行了增强,称之为 Enhanced object literals(增强对象字面量)。

字面量的增强主要包括下面几部分:

  1. 属性的简写:Property Shorthand
  2. 方法的简写:Method Shorthand
  3. 计算属性名:Computed Property Names
var name = "why"
var age = 18

var obj = {
  // 1.property shorthand(属性的简写)
  name,
  age,

  foo: function() {
    console.log(this)
  },
  // 2.method shorthand(方法的简写)
  bar() {
    console.log(this)
  },
  baz: () => {
    console.log(this)
  },

  // 3.computed property name(计算属性名), 也就是动态属性名的意思
  [name + 123]: 'hehehehe'
}

obj.baz()
obj.bar()
obj.foo()

// obj[name + 123] = "hahaha"
console.log(obj)

解构Destructuring

ES6中新增了一个从数组或对象中方便获取数据的方法,称之为解构Destructuring。我们可以划分为:数组的解构和对象的解构。

  1. 数组的解构
var names = ["abc", "cba", "nba"]
// var item1 = names[0]
// var item2 = names[1]
// var item3 = names[2]

// 对数组按顺序解构,使用[]
var [item1, item2, item3] = names
console.log(item1, item2, item3)

// 解构后面的元素
var [, , itemz] = names
console.log(itemz)

// 解构出一个元素,后面的元素放到一个新数组中
var [itemx, ...newNames] = names
console.log(itemx, newNames)

// 解构设置默认值
// 数组第四个元素是没有的,如果没有就使用默认值
var [itema, itemb, itemc, itemd = "aaa"] = names
console.log(itemd)
  1. 对象的解构
var obj = {
  name: "why",
  age: 18,
  height: 1.88
}

// 对象的解构,使用{},是没有顺序的
var { name, age, height } = obj
console.log(name, age, height)

// 只解构一个属性
var { age } = obj
console.log(age)

// 解构之后重命名
var { name: newName } = obj
console.log(newName)

// 解构之后设置默认值
var { address: newAddress = "广州市" } = obj
console.log(newAddress)

function foo(info) {
  console.log(info.name, info.age)
}
foo(obj)

// 将传入的对象直接进行解构
function bar({name, age}) {
  console.log(name, age)
}
bar(obj)

解构的应用场景

解构目前在开发中使用是非常多的,比如对函数的参数进行解构,对函数的返回值进行解构。