2023-08-09
JavaScript
00
请注意,本文编写于 470 天前,最后修改于 468 天前,其中某些信息可能已经过时。

目录

OOP编程
何为面向对象?
相关概念
原型链
es5的继承
es5继承的社区方案
1.1 直接原型链继承
1.2 原型式继承
2. 构造函数(对象冒充)继承
3.1 组合继承(原型链 + 借用构造函数
3.2 寄生组合继承
3.3 封装一个方法
4. 多继承-混入方式
es6 Class语法
基本语法
私有属性
私有属性的定义和使用
检测私有属性
静态块
解构会导致this指向错乱,如何固定class中的this?
new.target 属性
其他
Class 继承
class extends 语法
es6继承与es5继承的异同
super关键字
原生构造函数的继承
Mix模式的实现

本文将系统梳理JavaScript OOP编程,包含原型链、es5继承的社区方案、es6 Class语法、class 继承等内容。

详细思维导图链接点这里

OOP编程

何为面向对象?

定义:

  • 分析、设计、实现软件的一种编程思想
  • 基于对象的概念建立模型,模拟客观世界
  • 完整定义: 系统中一切事物皆对象。基于对象的概念建立模型,模拟客观世界,分析、设计、实现软件的编程思想。通过面向对象的理念使得计算机软件能与现实中的系统一一对应。
  • 面向对象(OOP)是一种常见的编程范式,常见的编程范式有POP、OOP、FP

相关概念

  • 类(Class):定义了一件事物的抽象特点,包含它的属性和方法
  • 对象(Object):类的实例,通过 new 生成
  • 面向对象(OOP)的三大特性:封装、继承、多态
  • 封装(Encapsulation):将对数据的操作细节隐藏起来,只暴露对外的接口。外界调用端不需要(也不可能)知道细节,就能通过对外提供的接口来访问该对象,同时也保证了外界无法任意更改对象内部的数据
  • 继承(Inheritance):子类继承父类,子类除了拥有父类的所有特性外,还有一些更具体的特性
  • 多态(Polymorphism):由继承而产生了相关的不同的类,对同一个方法可以有不同的响应。
  • 存取器(getter & setter):用以改变属性的读取和赋值行为
  • 修饰符(Modifiers):修饰符是一些关键字,用于限定成员或类型的性质。比如 public 表示公有属性或方法
  • 抽象类(Abstract Class):抽象类是供其他类继承的基类,抽象类不允许被实例化。抽象类中的抽象方法必须在子类中被实现
  • 接口(Interfaces):不同类之间公有的属性或方法,可以抽象成一个接口。接口可以被类实现(implements)。一个类只能继承自另一个类,但是可以实现多个接口

原型链

JS是基于原型链的继承

image.png

写一些代码验证上图

js
function Foo(){ } const f = new Foo(); console.log(f.__proto__ === Foo.prototype); console.log(Foo.prototype.__proto__ === Object.prototype) const o = new Object({}); console.log(o.__proto__ === Object.prototype); console.log(Object.__proto__ === null); console.log(Function.prototype.__proto__ === Object.prototype); console.log(Object.__proto__ === Function.prototype); console.log(Function.__proto__ === Function.prototype); console.log(Foo.__proto__ === Function.prototype); // 恒等式 Function 与 Object 互为原型 console.log(Function.__proto__ === Object.__proto__);

es5的继承

在es5中

  • 如果是对象可以通过Object.create实现继承
  • 如果是方法需要构造函数继承和原型继承
js
/* 手动继承 */ function Rectangle(length, width) { this.l = length this.w = width } Rectangle.prototype.getArea = function () { return this.l * this.w } function Square(length) { // 构造函数继承 Rectangle.call(this, length, length) } // 原型继承 Square.prototype = Object.create(Rectangle.prototype, { constructor: { // 修复constructor指向,不然使用instanceof就有问题 value: Square } }) var square = new Square(3); // square.__proto__.__proto__ = Rectangle.prototype; square.__proto__.constructor = Square console.log(square.getArea()) console.log(square instanceof Square) console.log(square instanceof Rectangle)

es5继承的社区方案

社区总结了一些继承的方式,关键点围绕构造函数和原型。

1.1 直接原型链继承

js
function Game(name){ this.name = name ?? 'LOL' this.skins = ['s'] } Game.prototype.getName = function() { return this.name; } function LOL() {} LOL.prototype = new Game(); LOL.prototype.constructor = LOL; /* 本质是重写原型链 缺点: 1) 原型链上的方法和属性都被共享了 2) 子类实例化的时候无法向父类传参 */ const game1 = new LOL(); const game2 = new LOL(); game1.skins.push('ss'); console.log(game2.skins); // 原型链上的方法和属性是共享的

1.2 原型式继承

Object.create(proto[, propertiesObject]) 第一个参数是父类原型,第二个参数是子类原型

js
var person = { name: 'Yvette', hobbies: ['reading'] } var person1 = Object.create(person); person1.name = 'Jack'; person1.hobbies.push('coding'); var person2 = Object.create(person); person2.name = 'Echo'; person2.hobbies.push('running'); console.log(person.hobbies); // [ 'reading', 'coding', 'running' ] console.log(person1.hobbies); // [ 'reading', 'coding', 'running' ] // 补充 Object.create原理 function create(super, properties) { function F(){} F.prototype = new super(); var f = new F(); properties && Object.defineProperties(F, properties); return f; }

2. 构造函数(对象冒充)继承

js
function SuperType(name) { this.name = name; this.colors = ['pink', 'blue', 'green']; } SuperType.prototype.work = function() { console.log(`I am ${this.name}, I can work`); } function SubType(name) { SuperType.call(this, name); } let instance1 = new SubType('Yvette'); instance1.colors.push('yellow'); console.log(instance1.colors); // ['pink', 'blue', 'green', yellow] let instance2 = new SubType('Jack'); console.log(instance2.colors); // ['pink', 'blue', 'green'] w.run() // 已经继承造函数里面的属性和方法 w.work() // 但是没有继承原型链上面的属性和方法

3.1 组合继承(原型链 + 借用构造函数

js
function Game() { this.name = 'LOL' this.skins = ['s'] } // 继承属性 function LOL() { Game.call(this, arguments); } // 遗传方法 LOL.prototype = new Game(); LOL.prototype.constructor = LOL; /* 优点: 可以向超类传递参数 每个实例都有自己的属性 实现了函数复用 缺点是什么(考虑一个问题应该从功能、性能、可读性三方面讲) 父类构造方法执行了两次 */

3.2 寄生组合继承

js
function Game() { this.name = 'LOL' this.skins = ['s'] } // 继承属性 function LOL() { Game.call(this, arguments); } // 遗传方法 LOL.prototype = Object.create(Game.prototype); LOL.prototype.constructor = LOL; // 优点: 解决了父类构造方法执行了两次的问题

扩展: 有道面试题: {}, new Object()Object.create的区别

3.3 封装一个方法

function extend(subClass, superClass) { function F() {} F.prototype = superClass.prototype; subClass.prototype = new F(); subClass.prototype.constructor = subClass; subClass.superClass = superClass.prototype; if(superClass.prototype.constructor === Object) { superClass.prototype.constructor = superClass; } } // 测试用例 function Person(name, age) { // 父类构造函数 this.name = name this.age = age this.sayIntroduce = function() { // 实例方法 console.log(`My name is ${name}, I am ${age} years old`) } } Person.prototype.eat = function () { console.log('Person.prototype.eat 我是个吃货') } function Student(name, age, school) { Student.superClass.constructor.call(this, name, age); this.school = school this.say = function () { // 实例方法 console.log('爸爸妈妈在上班,我上幼儿园') } } extend(Student, Person); var feifei = new Student('feifei', '3', '乡村幼儿园') console.log(feifei.age) // 子类实例属性 console.log(feifei.school) // 父类实例属性 console.log(feifei.say()) // 子类实例方法 console.log(feifei.sayIntroduce()) // 父类实例方法 console.log(feifei.eat()) // 父类原型方法

4. 多继承-混入方式

js
function MyClass() { SuperClass.call(this); OtherSuperClass.call(this); } // 继承一个类 MyClass.prototype = Object.create(SuperClass.prototype); // 混合其它 Object.assign(MyClass.prototype, OtherSuperClass.prototype); // 重新指定constructor MyClass.prototype.constructor = MyClass; MyClass.prototype.myMethod = function() { // do something };

es6 Class语法

基本语法

js
class Foo { age = 12; // 定义实例属性新方式 建议写在头部 get school () { // 定义实例属性 支持 get set 存取器 return '清华'; } // 构造函数, 不写默认会填充 constructor(name) { // 构造函数可以定义实例方法 this.name = name; // return this // 默认返回this } /** * 原型方法 相当于es5 Object.defineProperty(Foo, 'say', { configurable: true, writable: true, enumerable: false, value: ƒ say() }) */ say(){ console.log(`Hello, I am ${this.name}`) } // 不管是实例还是静态是属性或方法 都支持属性简写 // [methodName] () {} // 静态方法 不可枚举 static staticMethod(){} // 静态属性 可枚举 static staticProp = 'staticProp' } const foo = new Foo('zs'); // 必须new const foo2 = new Foo('ls'); foo.__proto__ === foo2.__proto__; // 与es5继承一样,原型是共享的 // 支持class表达式 const MyClass = class Me { };

私有属性

  • 传统方案
    • “_”命名
    • 移除到外部
    • 利用symbol
  • ES2022 增加了class私有属性 以#开头

私有属性的定义和使用

js
/* 私有属性定义与使用 */ class Bar { #age = 101; // 私有属性 #getAge() { // 私有方法 return this.#age; } // 私有属性也支持存取器 get #x() {} /** * 私有属性和方法使用方式一:在内部(构造函数或原型方法中)通过this引用 * 并且必须知道名字才能使用 无法通过Reflect.ownKeys获取 */ constructor() { console.log(this.#age, this.#getAge()); console.log(Reflect.ownKeys(this)); console.log(Object.getOwnPropertyDescriptor(this, "#age")); } } const bar = new Bar(); // console.log(bar.#age); // 严格模式下报错 // console.log(bar.#getAge()); // 严格模式下报错 // 私有属性和方法使用方式二: 类内部的静态方法中 // 私有属性不限于从this引用,只要是在类的内部,实例也可以引用私有属性 class Foo { #privateValue = 42; static getPrivateValue(foo) { return this.#privateValue; } } Foo.getPrivateValue(new Foo()); // 42 // 私有属性必须声明才能使用,否则编译时就报错, // 访问不存在的私有属性也一样编译时就报错 // 私有静态属性及方法 与 私有属性及方法的 用法特性一致

检测私有属性

js
// 可以通过静态方法try/catch的方式检测是否存在私有属性 // es2022 改进了 in 可以检测私有属性了 class C { #brand; static isC(obj) { /* try { obj.#brand; return true; } catch { return false; } */ return #brand in obj; } } // 子类从父类继承的私有属性,也可以使用in运算符来判断。 class A { #foo = 0; static test(obj) { console.log(#foo in obj); } } class SubA extends A {} A.test(new SubA()); // true // 注意,in运算符对于Object.create()、Object.setPrototypeOf形成的继承, // 是无效的,因为这种继承不会传递私有属性。 class A { #foo = 0; static test(obj) { console.log(#foo in obj); } } const a = new A(); const o1 = Object.create(a); A.test(o1); // false A.test(o1.__proto__); // true const o2 = {}; Object.setPrototypeOf(o2, a); A.test(o2); // false A.test(o2.__proto__); // true

静态块

  • 静态块用于静态代码初始化
  • 静态块的另一个作用是将私有属性与类的外部代码分享
js
// 静态块 处理静态属性需要初始话的逻辑 class polygon { static x = ...; static y; static { try { this.y = doSomethingWith(this.x) } catch { this.y = ...; } } } // 静态块 将私有属性与类的外部代码分享 let getX; class D { #x = 1; static { getX = (inst) => inst.#x; } } console.log(getX(new D())) // 1

解构会导致this指向错乱,如何固定class中的this?

  1. 箭头函数
  2. Proxy 劫持 放入缓存池时进行bind

new.target 属性

如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined

js
function Person(name) { if (new.target === Person) { this.name = name; } else { throw new Error('必须使用 new 命令生成实例'); } } var p1 = new Person('张三'); // ok var p2 = Reflect.construct(Person, ['ls']); // ok p2 instanceof Person // true Person.call(person, '张三'); // 报错 // Class 内部调用new.target,返回当前 Class class A { constructor() { console.log(new.target === A); // true } } class B extends A {} new B(); // false // 子类继承父类时,new.target会返回子类 // 这个特性用于创建抽象类

其他

  • 类和模块的内部,默认就是严格模式
  • 不存在提升,可重复声明
  • 如果定义了symbol.iterator方法,该类创建的实例就可以遍历

Class 继承

class extends 语法

js
class A { constructor(name, age) { this.name = name; this.age = age } } class B extends A { // 构造函数可以省略 默认填充如下内容 constructor() { super(...arguments) } } class C extends B { constructor(...args) { /* 一但写了构造函数就不能省略super() 这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造, 得到与父类同样的实例属性和方法,然后再对其进行加工,添加子类自己的实例属性和方法 */ super(...args); // 只有调用super()之后才可以使用this关键字,否则报错 console.log(this.age); } } // 私有属性或(方法包含静态的)都不能继承 class AA { #a = 1 #getA() {return this.#a} static #b = 'static prop'; static #getB(){return this.#b}; } class BB extends AA { constructor() { super(); console.log(this.#a); // 编译时报错 this.#getA(); // 编译时报错 } static log() { console.log(this.#b); // 编译时报错 console.log(this.#getB()); // 编译时报错 } } // 想要访问私有属性需要借助普通方法 class Foo { #p = 1; getP() { return this.#p; } setP(num) { this.#p=num; } } class Bar extends Foo { constructor() { super(); } } let bar1 = new Bar(); let bar2 = new Bar(); bar1.setP(123); bar1.getP(); // 123 bar2.getP(); // 1 // 这个可否理解私有实例属性是被继承的?

es6继承与es5继承的异同

js
class A{} class B extends A{} let b = new B();
  1. es6的继承包含静态继承
    • B.__proto__ === A; // 类继承 静态属性的继承是浅copy
    • B.prototype.__proto__ === A.prototype // 原型继承继承
    • 构造函数继承
  2. es6内部定义的方法都是不可枚举的
  3. 机制不同 es6 "继承在前,实例在后" 对原生构造函数的继承继承就能体现这一点。
  4. 与 ES5 一样,类的所有实例共享一个原型对象。

super关键字

  • 必须指明是作为函数还是作为对象使用
  • 作为函数时只能用在子类的构造函数中
    • 由于super()在子类构造方法中执行时,子类的属性和方法还没有绑定到this,所以如果存在同名属性,此时拿到的是父类的属性。
  • 作为对象时
    • 在普通方法中指向父类的原型对象,
    • 在静态方法中指向父类
    • super.x 取值,可以取到父类原型上的属性或方法,取不到实例上的属性或方法
    • super.x 赋值,修改的是子类的实例属性
    • super.x()作为方法调用父类方法,父类静态方法中的this指向子类,父类普通方法中的this指向子类实例

由于对象总是继承其他对象的,所以可以在任意对象中使用super

js
var obj = { toString() { return `MyToString ${super.toString()}`; } } obj.toString(); // 'MyToString [object Object]'

原生构造函数的继承

  • es6与es5继承的行为差异
  • 带版本功能的MyArray
  • 继承Object的子类,有一个行为差异
js
class VersionedArray extends Array { constructor() { super(); this.history = [[]]; } commit() { this.history.push(this.slice()); } revert() { const last = this.history.pop() || []; this.splice(0, this.length, ...last); } }

Mix模式的实现

js
function mix(...mixins) { class Mix { constructor() { for (let mixin of mixins) { copyProperties(this, new mixin()); // 拷贝实例属性 } } } for (let mixin of mixins) { copyProperties(Mix, mixin); // 拷贝静态属性 copyProperties(Mix.prototype, mixin.prototype); // 拷贝原型属性 } return Mix; } function copyProperties(target, source) { for (let key of Reflect.ownKeys(source)) { if ( key !== 'constructor' && key !== 'prototype' && key !== 'name' ) { let desc = Object.getOwnPropertyDescriptor(source, key); Object.defineProperty(target, key, desc); } } }

本文作者:郭敬文

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!