本文将系统梳理JavaScript OOP编程,包含原型链、es5继承的社区方案、es6 Class语法、class 继承等内容。
详细思维导图链接点这里
定义:
JS是基于原型链的继承
写一些代码验证上图
jsfunction 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中
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)
社区总结了一些继承的方式,关键点围绕构造函数和原型。
jsfunction 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); // 原型链上的方法和属性是共享的
Object.create(proto[, propertiesObject]) 第一个参数是父类原型,第二个参数是子类原型
jsvar 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;
}
jsfunction 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() // 但是没有继承原型链上面的属性和方法
jsfunction Game() {
this.name = 'LOL'
this.skins = ['s']
}
// 继承属性
function LOL() {
Game.call(this, arguments);
}
// 遗传方法
LOL.prototype = new Game();
LOL.prototype.constructor = LOL;
/*
优点:
可以向超类传递参数
每个实例都有自己的属性
实现了函数复用
缺点是什么(考虑一个问题应该从功能、性能、可读性三方面讲)
父类构造方法执行了两次
*/
jsfunction 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
的区别
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()) // 父类原型方法
jsfunction 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
};
jsclass 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 { };
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
如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined
jsfunction 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
方法,该类创建的实例就可以遍历jsclass 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
// 这个可否理解私有实例属性是被继承的?
jsclass A{}
class B extends A{}
let b = new B();
B.__proto__ === A
; // 类继承 静态属性的继承是浅copyB.prototype.__proto__ === A.prototype
// 原型继承继承super()
在子类构造方法中执行时,子类的属性和方法还没有绑定到this
,所以如果存在同名属性,此时拿到的是父类的属性。super.x
取值,可以取到父类原型上的属性或方法,取不到实例上的属性或方法super.x
赋值,修改的是子类的实例属性super.x()
作为方法调用父类方法,父类静态方法中的this指向子类,父类普通方法中的this
指向子类实例由于对象总是继承其他对象的,所以可以在任意对象中使用super
jsvar obj = {
toString() {
return `MyToString ${super.toString()}`;
}
}
obj.toString(); // 'MyToString [object Object]'
MyArray
Object
的子类,有一个行为差异jsclass 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);
}
}
jsfunction 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 许可协议。转载请注明出处!