# 面向对象

  面向过程的时候,我们关注的是每一个元素的关系、顺序。

  面向对象的时候,我们关注于找到一个对象来帮我们了做事情

# 一、创建对象

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 的原型是 nullnull 没有任何属性和方法,也没有自己的原型,是原型链的尽头。

Object.getPrototypeOf(Object.prototype) // null

  JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的 Object.prototype 还是找不到,则返回 undefined。

var info = {}
// 相当于
// var info = new Ojbect()

console.log(info.__proto__ === Object.prototype) // true

# 2、显/隐式原型

  通过上面的原型链示意图,我们发现使用Person.prototypefoo.__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.prototypeconstructor属性,能够被构造函数 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
更新于 : 7/8/2024, 10:21:14 AM