# 面向对象
面向过程的时候,我们关注的是每一个元素的关系、顺序。
面向对象的时候,我们关注于找到一个对象来帮我们了做事情
# 一、创建对象
TIP
面向对象编程的第一步,就是要生成对象。我们通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。
在 java 和 c++中都有类(class),类就是对象的模板,对象就是类的实例。
但是在 JavaScript 中,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)
# 1、字面量{}
var person = {
  name: 'lencamo',
  age: 22,
  sayHello: function () {
    console.log('hello!')
  }
}
# 2、工厂函数
TIP
对象是一个容器,封装了属性(property)和方法(method)
function createPerson(name, age) {
  return {
    name: name,
    age: age
  }
}
var person = createPerson('lencamo', 22)
# 3、构造函数 ✨
我们在使用构造函数创建对象时,要注意一下事项:
- 不用写返回
- 建议首写字母大写(与普通函数做区分)
- 函数体内使用this关键字表示生成的对象实例
- 生成对象需要用new命令
new 命令的执行原理
function Person(name, age) {
  this.name = name
  this.age = age
  //如果返回的不是Object类型,将忽略这个return 返回之前创建的空对象
  // return '张三'
  //如果返回的是Object类型,直接返回这个对象
  return {
    name: '小花',
    age: 22
  }
}
function _new(person, ...rest) {
  // 1、创建一个空对象,作为将要返回的对象实例
  // var obj = {}
  // 2、将这个空对象的原型,指向构造函数的 prototype 属性
  // obj.__proto__ = person.prototype;
  //上述两步可以合为一步 :
  var obj = Object.create(person.prototype)
  // 3、将person的this指向空对象,并运行person函数(一句话:使用apply)
  var res = person.apply(obj, rest)
  // 4、模拟return
  // 如果返回结果是对象就直接返回,否则返回 obj 对象(默认返回obj对象)
  return typeof res === 'object' && res != null ? res : obj
}
var lencamo = _new(Person, '小明', 22)
console.log(lencamo)
// console.log(lencamo.name);  //小明
// console.log(lencamo.age);   //15
// 自定义构造函数
function Person(name, age) {
  this.name = name
  this.age = age
}
var person1 = new Person('lencamo', 22)
// js 内置构造函数
var person2 = new Object()
person2.name = 'lencamo'
person2.age = 22
严格模式
1、问题描述:
// 1、普通模式
var Vehicle = function () {
  this.price = 1000
}
Vehicle() // undefined 调用
price // 1000 this执行全局对象windows
若忘记加上 new,构造函数会变为普通函数,此时 this 代表的是全局对象,比较危险 🤔!!
2、问题解决:
严格模式下,会禁止 this 关键字指向全局对象
function f() {
  'use strict'
  console.log(this === undefined)
}
f() // true
  由于严格模式中,函数内部的 this 不能指向全局对象window,默认等于 undefined。并且 JavaScript 不允许对 undefined 添加属性。
// 2、严格模式
var Vehicle = function () {
  'use strict'
  // 等效于
  // if (!(this instanceof Vehicle)) {
  //   return new Vehicle();
  // }
  // 给出提示
  // if (!new.target) {
  //   throw new Error('请使用 new 命令调用!');
  // }
  this.price = 1000
}
Vehicle() // 报错:Cannot set properties of undefined
price // 报错:price is not defined
return 问题 ✍
var Person = function () {
  this.age = 21
}
Person() // undefined
如果 return 后面跟着一个对象,new 命令会返回 return 语句指定的对象;否则,就会不管 return 语句,返回 this 对象。
var Person = function () {
  this.age = 21
  return 666
}
new Person() // Vehicle {age: 21}
var Person = function () {
  this.age = 21
  return { name: 'lencamo' }
}
new Person() // {name: "lencamo"}
# 4、Object.create()
我们也可以使用现有的对象作为模板,生成新的实例对象。
如下:对象 person1 是 person2 的模板,后者继承了前者的属性和方法
var person1 = {
  name: '张三',
  age: 38,
  greeting: function () {
    console.log("Hi! I'm " + this.name + '.')
  }
}
var person2 = Object.create(person1)
# 5、js 类
其实,js 中的类(ES6 引入)就是对象构造器的一个语法糖
class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
}
var person1 = new Person('lencamo', 22)
# 二、原型链
大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。
传统上,JavaScript 语言的继承不通过 class,而是通过“原型对象”(prototype)实现。
# 原型对象
TIP
前面我们已经了解到在 JavaScript 中,对象的模板不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)
JavaScript 可以通过构造函数生成对象模板。但使用过程中也有一个缺点:
同一个构造函数的多个实例之间,无法共享属性
function Cat(name, color) {
  this.name = name
  this.color = color
  // 公共属性,但却反复生成🤔
  this.meow = function () {
    console.log('喵喵')
  }
}
var cat1 = new Cat('大毛', '白色')
var cat2 = new Cat('二毛', '黑色')
cat1.meow === cat2.meow
TIP
JavaScript 的继承机制:原型对象的所有属性和方法,都能被实例对象共享
代码示例
function Animal(name) {
  this.name = name
}
Animal.prototype.color = 'white'
var cat1 = new Animal('大毛')
var cat2 = new Animal('二毛')
cat1.color // 'white'
cat2.color // 'white'
# 1、原型链
我们要注意的是原型对象也是对象。
  所以原型对象也有自己的原型,然后:对象到原型,原型到原型……最终到Object.prototype,这就是我们说的原型链。
拓展
Object.prototype 的原型是 null,null 没有任何属性和方法,也没有自己的原型,是原型链的尽头。
Object.getPrototypeOf(Object.prototype) // null

JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的 Object.prototype 还是找不到,则返回 undefined。
var info = {}
// 相当于
// var info = new Ojbect()
console.log(info.__proto__ === Object.prototype) // true
# 2、显/隐式原型
  通过上面的原型链示意图,我们发现使用Person.prototype 和 foo.__proto__都可以得到原型对象。为了区分,我们可以
- 把prototype叫做显示原型
任何(非箭头)函数都有自己的 prototype 属性,即函数的显示原型
- 把__proto__叫做隐式原型(可以存在多个实例对象)
任何对象都有自己默认的隐式原型
提示
当我们使用 new 操作符调用函数时,创建的实例对象 foo 的隐式原型会指向被调用函数的显示原型上
即 ✍:
obj.__proto__ = F.prototype
new 命令的执行原理
function Person(name, age) {
  this.name = name
  this.age = age
  //如果返回的不是Object类型,将忽略这个return 返回之前创建的空对象
  // return '张三'
  //如果返回的是Object类型,直接返回这个对象
  return {
    name: '小花',
    age: 22
  }
}
function _new(person, ...rest) {
  // 1、创建一个空对象,作为将要返回的对象实例
  // var obj = {}
  // 2、将这个空对象的原型,指向构造函数的 prototype 属性
  // obj.__proto__ = person.prototype;
  //上述两步可以合为一步 :
  var obj = Object.create(person.prototype)
  // 3、将person的this指向空对象,并运行person函数(一句话:使用apply)
  var res = person.apply(obj, rest)
  // 4、模拟return
  // 如果返回结果是对象就直接返回,否则返回 obj 对象(默认返回obj对象)
  return typeof res === 'object' && res != null ? res : obj
}
var lencamo = _new(Person, '小明', 22)
console.log(lencamo)
// console.log(lencamo.name);  //小明
// console.log(lencamo.age);   //15
// new Function()
// new Object()
function Person() {
  //
}
Person.prototype.age = 20
var foo1 = new Person()
var foo2 = new Person()
console.log(foo1.__proto__ === Person.prototype) // true
console.log(foo1.__proto__ === foo2.__proto__) // true
// ============
console.log(foo1.__proto__.age)
console.log(foo1.age) // 简写
# 3、对象原型(__proto__)
   前面,我们已经接触了Object.getPrototypeOf(),常见的还有Object.prototype.__proto__等
Object.getPrototypeOf()和__proto__的作用一样,用于获取对象原型
区别 🤔
  -getPrototypeOf 是标准的 ECMAScript 方法,而 __proto__ 是非标准的(但大多数浏览器是支持__proto__的)
- getPrototypeOf是一个函数,用于获取指定对象的原型(即- [[Prototype]]属性的值)
- __proto__是一个访问器属性,可以直接访问对象的原型(实际上是- [[Prototype]]的一个引用)
- 区别
var obj = {}
var p = {}
obj.__proto__ = p
Object.getPrototypeOf(obj) === p // true
obj.__proto__ === p // true
- __proto__使用
TIP
JS 会先在实例对象上找目标属性,如果没有则在原型对象上继续查找。
var obj = {
  name: 'lencamo'
}
obj.age = 21
obj.__proto__.age = 20
console.log(obj.age) // 21
# 4、函数原型(prototype)
   -prototype 是函数对象特有的属性。
TIP
  JavaScript 规定,每个函数都有一个 prototype 属性,指向一个对象
function f() {}
typeof f.prototype // "object"
- 区别
var obj = {}
function Person() {
  //
}
console.log(obj.__proto__)
console.log(Person.__proto__)
- prototype 使用
原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。
TIP
JS 会先在实例对象上找目标属性,如果没有则在原型对象上继续查找。
function Person() {
  //
}
Person.prototype.age = 30
const foo = new Person()
console.log(foo.age) // 输出: 30
prototype 应用 —— 实例方法
通过构造函数在其原型对象上挂载的一些属性和方法,底层的实例对象是可以进行直接调用的。
function Star(uname, age) {
  this.uname = uname
  this.age = age
}
Star.prototype.sing = function () {
  console.log(this.uname + ' 哟哟!切克闹!')
}
var ldh = new Star('刘德华', 33)
ldh.sing()
拓展:—— 类方法
Star.running = function () {
  console.log('run run run! ')
}
Star.running()
# 5、原型属性 constructor
  
   -prototype 对象有一个 constructor 属性,默认指向 prototype 对象所在的构造函数。
function Person() {
  //
}
console.log(Person.prototype)
console.log(Person.prototype.constructor)
console.log(Person.prototype.constructor === Person) // true
instanceof 运算符
  -instanceof运算符的所有是验证 某些对象实例 / 原型对象是否是某个构造函数产生的
function Person() {
  //
}
var foo = new Person()
foo instanceof Person
  并且,由于constructor属性定义在prototype对象上面,意味着可以被所有实例对象继承。
即:构造函数 Person()的 原型对象
Person.prototype的constructor属性,能够被构造函数 Person()的 实例对象foo所继承。
function Person() {
  //
}
var foo = new Person()
Person.prototype.constructor // 构造函数 Person()
foo.constructor // 构造函数 Person()
为了理解,我们甚至可以重写一些 constructor:
function Person() {
  //
}
console.log(Object.getOwnPropertyDescriptor(Person.prototype, 'constructor'))
# 6、原型继承图示
http://www.mollypages.org/tutorials/js.mp
 
 代码理解
function Foo() {}
var f1 = new Foo()
// function Object() {}
// var o1 = new Object()
var o1 = {}
// function Function() {}
// =============
// 1、实例对象
console.log(f1.__proto__ === Foo.prototype)
console.log(o1.__proto__ === Object.prototype)
console.log(Foo.__proto__ === Function.prototype)
console.log(Object.__proto__ === Function.prototype)
console.log(Function.__proto__ === Function.prototype)
// 2、Object 是所有类的父类
console.log(Foo.prototype.__proto__ === Object.prototype)
console.log(Function.prototype.__proto__ === Object.prototype)
console.log(Object.prototype.__proto__ === null)
# 7、原型对象方法
- Object.hasOwnProperty( )
- in 操作符
var obj1 = {
  name: 'hello',
  age: 20
}
var obj2 = Object.create(obj1)
obj2.address = 'china'
obj2.discrition = 'good good good'
// console.log(obj2)
// 1、hasOwnProperty
console.log(obj2.hasOwnProperty('name')) // false
console.log(obj2.hasOwnProperty('address')) // true
// 2、in操作符
console.log('name' in obj2) // true
console.log('address' in obj2) // true
- isPrototypeOf( )
- instanceof 操作符
function Person() {}
function Student() {}
var obj = Object.create(Person.prototype)
Student.prototype = obj
var stu = new Student()
// 1、isPrototypeOf
console.log(Student.prototype.isPrototypeOf(stu))
// 对象间
console.log(obj1.isPrototypeOf(obj2)) // true
// 2、instanceof
console.log(stu instanceof Student) // true
# 三、原型/类继承
通过原型链的学习,我们已经知道了 Object 是所有类的父类
TIP
在 JavaScript 中,对象的模板不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)
# 继承发展史 👍
代码示例
function Person(name) {
  this.name = name
}
Person.prototype.running = function () {
  console.log(this.name + ' running')
}
function Student(name, age) {
  // 重点(属性继承实现) ---- 借用构造函数
  Person.call(this, name)
  this.age = age
}
// 重点(方法继承实现) ---- 原型式继承
// 方案1
var p = new Person()
Student.prototype = p
// ==========
// 重点(方法继承实现) ---- 组合式继承
// 方案2
// var obj = {}
// obj.__proto__ = Person.prototype
// Student.prototype = obj
// 方案3
// var obj = {}
// Object.setPrototypeOf(obj, Person.prototype)
// Student.prototype = obj
// ==========
// 方案4
// function F() {}
// F.prototype = Person.prototype
// Student.prototype = new F()
// 方案5
// var obj = Object.create(Person.prototype)
// Student.prototype = obj
Student.prototype.studying = function () {
  console.log(this.name + ' studying')
}
var stu1 = new Student('lencamo', 22)
stu1.running()
# 1、ES5 - 构造函数继承
缺点:
- 不能继承父类方法
- 继承的父类属性不是通过原型链共享的方式获取的,而是通过复制获取的,这样会导致内存浪费
function Person(name) {
  this.name = name
}
Person.prototype.running = function () {
  console.log(this.name + ' is running')
}
function Student(name, age) {
  Person.call(this, name) // 使用构造函数继承属性
  // this.name = name
  this.age = age
}
var stu1 = new Student('lencamo', 22)
console.log(stu1.name)
# 2、ES5 - 原型链继承
缺点:
子类共享了父类原型的属性和方法,一旦父类有引用类型,其中任何一个对象实例修改了这个引用类型的属性值,将会影响到其他所有的对象实例。
function Person(name) {
  this.name = name
}
Person.prototype.running = function () {
  console.log(this.name + ' is running')
}
function Student(name, age) {
  this.name = name
  this.age = age
}
// =====
Student.prototype = new Person() // 直接通过赋值的方式
// =====
var stu1 = new Student('lencamo', 22)
stu1.running()
# 3、ES5 - 寄生组合继承
重写 constructor
Person.prototype.msg = 'hello, boys'
Person.prototype.age = 20
Person.prototype.running = function () {}
等效于
Person.prototype = {
  message: 'hello, boys',
  age: 20,
  running: function () {}
  // constructor: Person
}
Object.defineProperty(Person.prototype, 'constructor', {
  writable: true,
  enumerable: false,
  configurable: true,
  value: Person
})
寄生组合继承,结合了构造函数继承 和 原型继承
封装一个 inherit_utils.js
function createObject(superTypePrototype) {
  // 方案1
  // var obj = {}
  // obj.__proto__ = superTypePrototype
  // Student.prototype = obj
  // 方案2
  // var obj = {}
  // Object.setPrototypeOf(obj, superTypePrototype)
  // Student.prototype = obj
  // 方案3
  // function F() {}
  // F.prototype = superTypePrototype
  // return new F()
}
function inherit(Subtype, Supertype) {
  // Subtype.prototype = createObject(Supertype.prototype)
  // 方案4
  Subtype.prototype = Object.create(Supertype.prototype)
  Object.defineProperty(Subtype.prototype, 'constructor', {
    enumerable: false,
    configurable: true,
    writable: true,
    value: Subtype
  })
}
function Person(name) {
  this.name = name
}
Person.prototype.running = function () {
  console.log(this.name + ' running')
}
function Student(name, age) {
  // 1、调用父类构造函数 继承属性
  Person.call(this, name)
  this.age = age
}
// 2、使用 对象/方法(创建一个两者间的原型对象)、Object.create()等 间接实现 原型继承
inherit(Student, Person)
Student.prototype.studying = function () {
  console.log(this.name + ' studying')
}
var stu1 = new Student('lencamo', 22)
stu1.running()
# 4、ES6 - class 类继承
但是为了让其他语言的开发者更快的适应 js 语法,ES6 中提供了另一种写法:
class 与 构造函数 的 写法对比
- 构造函数方式
function Person(name, age) {
  this.name = name
  this.age = age
}
Person.prototype.say = function () {
  console.log(this.name, this.age)
}
var obj = new Person('lencamo', 22)
obj.name
obj.say()
console.log(Person.prototype === obj.__proto__) // true
- class 方式
class Person {
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  say() {
    console.log(this.name, this.age)
  }
}
var obj = new Person('lencamo', 22)
obj.name
obj.say()
// console.log(Person.prototype.say)
console.log(Person.prototype === obj.__proto__) // true
注意
- 在 JavaScript 中是不支持多继承的
解决方案:混入(Mixin)模式、组合继承
多继承 - 混入模式
- 方式 1
function mixin(target, ...sources) {
  Object.assign(target, ...sources)
}
const canEat = {
  eat() {
    console.log('Eating...')
  }
}
const canSleep = {
  sleep() {
    console.log('Sleeping...')
  }
}
function Person() {
  // ...
}
mixin(Person.prototype, canEat, canSleep)
const person = new Person()
person.eat() // 输出: Eating...
person.sleep() // 输出: Sleeping...
- 方式 2
function mixinEat(BaseClass) {
  return class extends BaseClass {
    eat() {
      console.log('Eating...')
    }
  }
}
function mixinSleep(BaseClass) {
  return class extends BaseClass {
    sleep() {
      console.log('Sleeping...')
    }
  }
}
function Person() {
  // ...
}
class newPerson extends mixinEat(mixinSleep(Person)) {
  //
}
var p = new newPerson()
p.eat()
p.sleep()
- super 关键字可以直接用于在子类中调用父类中的方法或属性等
- 方法可以直接中类中定义、继承时直接使用 extends 即可
// 高内聚低耦合
class Person {
  static pageTitle = '青春梦想'
  static ownShow = function () {
    //
  }
  constructor(name, age) {
    this.name = name
    this.age = age
  }
  say() {
    console.log(this.name, this.age)
  }
}
class Student extends Person {
  constructor(name, age, scope) {
    super(name, age)
    this.scope = scope
  }
  running() {
    super.say()
  }
  // 重写
  say() {
    console.log('对父类方法running进行重写')
  }
}
var obj = new Student('lencamo', 22, 150)
obj.say()
# 四、class 类拓展
# 1、访问器方法
对象中的访问器方法
// 方式1
var obj = {
  _name:'lencamo'
  // setter方法
  set name(value) {
    this._name = value
  }
  // getter方法
  get name() {
    return this._name
  }
}
// 方式2 ✔
var obj = {
  _name: 'lencamo'
}
Object.defineProperty(obj, 'name', {
  set: function() {
    //
  },
  get: function() {
    //
  }
})
class 中的访问器方法
class Person {
  constructor(name) {
    this._name = name
  }
  set name(value) {
    console.log('你修改了name')
    this._name = value
  }
  get name() {
    console.log('你获取了name')
    return this._name
  }
}
var stu1 = new Person('lencamo')
stu1.name = 'ren'
console.log(stu1.name)
访问器方法可以用来实现属性的读写控制,比如只读属性、只写属性、计算属性等。
class Circle {
  constructor(radius) {
    this._radius = radius
    this._title = '圆面积' // 只读属性
    this._pi = Math.PI // 只写属性
  }
  // 只读
  get getTitle() {
    return this._title
  }
  // 只写
  set setPi(value) {
    this._pi = value
  }
  // 计算属性
  get circleArea() {
    return this._pi * this._radius ** 2
  }
}
const myCircle = new Circle(5)
console.log(myCircle.getTitle) // 圆面积
console.log(myCircle.circleArea) // 78.53981633974483
myCircle.setPi = 3.14
console.log(myCircle.circleArea) // 78.5
# 2、静态方法
类方法(静态方法)可以直接通过类来调用,不需要实例化对象。
但个人感觉,使用静态方法的场景不多,建议还是用
原型方法或实例方法来实现功能。
构造函数中的原型方法
function Star(name, age) {
  this._name = name
  this._age = age
}
// 原型方法 🎈
Star.prototype.sing = function () {
  //
}
// 静态方法
Star.running = function () {
  //
}
var ldh = new Star('刘德华', 33)
ldh.sing()
// Star.running()
class Star {
  constructor(name, age) {
    this._name = name
    this._age = age
  }
  // 实例方法 🎈
  sing() {
    //
  }
  // 静态方法
  static running() {
    //
  }
}
var ldh = new Star('刘德华', 33)
ldh.sing()
Star.running()
# 3、多态性
ES6 中的类支持多态,它允许子类对象在实现继承的基础上对父类的方法进行重写,从而实现不同对象调用相同方法时表现出不同的行为。
class Animal {
  constructor(name) {
    this.name = name
  }
  eat() {
    console.log(this.name + ' is eating')
  }
}
class Dog extends Animal {
  constructor(name, age) {
    super(name)
    this.age = age
  }
  eat() {
    console.log(this.name + ' is eating dog food')
  }
}
class Cat extends Animal {
  constructor(name, age) {
    super(name)
    this.age = age
  }
  eat() {
    console.log(this.name + ' is eating cat food')
  }
}
var dog = new Dog('旺财', 3)
var cat = new Cat('喵星人', 2)
dog.eat() // 旺财 is eating dog food
cat.eat() // 喵星人 is eating cat food
# 4、抽象类
抽象类(abstract class)是一种特殊的类,它不能实例化,只能被继承,并且子类必须实现抽象类中定义的所有抽象方法。
abstract class Animal {
  constructor(name) {
    this.name = name
  }
  // 父类方法
  eat() {
    console.log(this.name + ' is eating')
  }
  // 抽象方法
  abstract sleep() {
    //
  }
}
class Dog extends Animal {
  constructor(name, age) {
    super(name)
    this.age = age
  }
  // 重写父类方法
  eat() {
    console.log(this.name + ' is eating dog food')
  }
  // 实现抽象方法
  sleep() {
    console.log(this.name + ' is sleeping')
  }
}
var dog = new Dog('旺财', 3)
dog.eat() // 旺财 is eating dog food
dog.sleep() // 旺财 is sleeping
// var animal = new Animal('lencamo') // 报错:不能实例化抽象类
# 5、接口
接口(interface)是一种特殊的抽象类,它不能实例化,只能被继承,并且子类必须实现接口中定义的所有属性和方法。
应用:TypeScript 接口,可以用来定义类的公共部分,可以强制要求子类必须实现接口中的所有属性和方法。
interface Person {
  name: string
  age: number
  say(): void
}
class Student implements Person {
  name: string
  age: number
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
  say() {
    console.log(this.name + ' is saying')
  }
}
var stu1 = new Student('lencamo', 22)
stu1.say() // lencamo is saying
