# 面向对象
面向过程的时候,我们关注的是每一个元素的关系、顺序。
面向对象的时候,我们关注于找到一个对象来帮我们了做事情
# 一、创建对象
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