js 设计模式:面向对象还是基于对象 | 我们真的需要"类"吗?
reference
主要内容参考:
你不知道的 js(上)
极客时间专栏-重学前端
阮一峰 es6
mdn web Docs
前言
最近突发奇想,想把关于对象的一些知识成体系的整理一下,于是记录了这篇博客。但内容确实比较繁杂,所以如果有勘误或者疑问欢迎评论区交流qwq,本人也只是三脚猫功夫
如果你有学过一门面向对象的语言,eg. C++、Java,那你一定对面向对象的核心技术不陌生:
- 封装
- 继承
- 多态
与之相对的,也带来了 class、new、extends 等关键字,用以描述类和对象的关系。
很自然的,我们会认为 js 中既然也提供了类似的关键字,那么就可以按照面向对象的方式来思考、设计我们的代码。大部分时候,我们写出的代码也符合我们的预期。但是,js 中提供的类真的是我们预想中的类吗?
也许我们期望的结果是“是的”,但实际上 js 中的类和我们想象中的类相距甚远。js 中的类是以另一种方式实现的,又或者说只是基于历史因素,对类设计模式的一种拙劣的模仿
虽然随着 es6 支持 class 关键字,我们不用再写一些丑陋的代码来模拟类,但实现的时候也是两个概念
pre- js 中对象的特征
在正式开始之前,我认为有必要回顾一下 js 中对象的概念
Grandy Booch《面向对象分析与设计》中对对象的描述:
- 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。
- 对象有状态:对象具有状态,同一对象可能处于不同状态之下。
- 对象具有行为:即对象的状态,可能因为它的行为产生变迁。
对于 1,一般而言,各种语言的对象唯一标识性都是用内存地址来体现的, 对象具有唯一标识的内存地址,所以具有唯一的标识。
js 也不例外:
var o1 = { a: 1 };
var o2 = { a: 1 };
console.log(o1 == o2); // false
对于状态和行为,不同语言会使用不同的术语来抽象描述它们,比如 C++ 中称它们为成员变量和成员函数,Java 中则称它们为属性和方法。
而在 js 中,2、3 被统一抽象为属性(函数也是对象,也就是一个普通属性),比如这里的 d 和 f,对 o 来说这就是他的两个普通属性
var o = {
d: 1,
f() {
console.log(this.d);
},
};
刚才提及了 js 的类也是对象(抛开 class 语法糖,类也是用 function 模拟的),而实际上有一种说法: js 万物皆为对象。
支持这种说法的理由是:
- 基本数据类型也是对象:
基本数据类型可以直接使用对象的方法和属性(包装对象) - 函数也是对象:(的一个子类型/可调用对象)
函数可以作为参数传递,也可以赋值给变量,也可以作为返回值。也就是所谓的一等公民 - 数组也是对象的一种类型,具备一些额外的行为
- 甚至还有正则表达式、日期对象、错误对象等
但其实这种说法也是不确切的,对象也只是 6/7 个基础类型之一。
我们会这么认为,或许是因为 js 中的对象具有高度的动态性,而这来源于 JavaScript 赋予了使用者在运行时为对象添改状态和行为的能力。
同时为了提高抽象能力,js 还提供了更抽象的数据属性和访问器属性(getter/setter)
pre- js 中的原型
除了对象以外,我们还需要回顾一下原型
- 每个对象都有私有字段 [[prototype]],它指向该对象的原型对象。(Object.create(null)创建的对象,该值为 null)
- 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。
(原型链到 Object.prototype 终止,它的值为 null)
let obj = {
a: 2,
};
let anotherObj = Object.create(obj);
console.log(anotherObj.a); //2
我们在 anotherObj 上显然没有定义 a,所以会继续访问原型,也就是 obj,obj 上有 a,所以返回 2
很自然的,想要测试链式查找,我们可能会写出如下代码:
obj.prototype.b = 3;
console.log(anotherObj.b); //TypeError: Cannot set properties of undefined (setting 'b')
这是什么情况?为什么 obj 没有 prototype 呢?其实是有的。
-
[[Prototype]]
:[[Prototype]]
是每个 JavaScript 对象内部的一个隐藏属性,用于指向该对象的原型。这是 JavaScript 中的一个内部机制,通常不可见,不能直接访问。[[Prototype]]
是对象原型的实际连接,用于查找属性和方法。- 通过
Object.getPrototypeOf(obj)
可以获取对象的[[Prototype]]
。
-
__proto__
:__proto__
是 JavaScript 中的一个非标准的属性,它是一种浏览器提供的方式,用于访问和修改对象的原型。__proto__
通常可以用来读取和设置对象的原型,但它不是 ECMAScript 标准的一部分。- 尽管
__proto__
在某些情况下可以用于操作原型,但不建议在生产代码中使用,因为它不是标准的,可能在不同的 JavaScript 环境中表现不一致。
我们在创建 obj 对象的时候,实际上创建了一个{}
空对象作为 obj 的原型
而刚才的 Object.create,只是把 obj 作为 anotherObj 的原型对象,而 obj 本身并不是一个原型
那么,我们就可以写出如下代码:
let obj = {
a: 2,
};
let anotherObj = Object.create(obj);
// obj.__proto__.b = 3; //可行,但不推荐
// Object.prototype.b = 3; //可行,通过原型链追溯都能找到
Object.getPrototypeOf(obj).b = 3; //可行,调api拿到obj的prototype再在这上面加属性
console.log(anotherObj.b);
这里只推荐第三种方式。这是获取原型对象的标准方法。
而且从原型链查找也是要耗费性能的,自然在最近的地方加是最好的,虽然在这里是一致的
console.log(Object.getPrototypeOf(obj) === Object.prototype); //true
基于类 vs 基于原型
好的,那么现在可以正式进入我们的主题了:
C++、Java 是基于类的编程,提倡使用一个关注分类和类之间关系开发模型。
总是先有类,再从类去实例化一个对象,类与类之间又可能会形成继承、组合等关系。
类将一组行为和属性封装在一起,方便复用
js 是基于原型的编程,提倡程序员去关注一系列对象实例的行为,而后才去关心如何将这些对象,划分到最近的使用方式相似的原型对象,而不是将它们分成类。
对象通过复制的方式创建新对象
类是对象的一种,它和对象一样,可以拥有属性和方法。(当然你也可以说 js 中方法也是属性的一种,只抽象为属性)
eg:
当我们看到老虎的时候,基于类的描述会使用猫科豹属豹亚种,而基于原型的描述会使用大猫
事实上,js 的机制更适合后者(甚至似乎一直在阻止你使用类设计模式),不过只要你足够了解 js 中的类,这两种方式都是可选的
不过注意,要用类设计模式最好使用 class 语法糖而非手动操作原型和构造函数!
抛开对类的模拟,当我们对大猫进行描述的时候,代码应该是这样的:
var cat = {
say() {
console.log("meow~");
},
jump() {
console.log("jump");
},
};
var tiger = Object.create(cat, {
//属性描述对象
say: {
writable: true,
configurable: true,
enumerable: true,
value: function () {
console.log("roar!");
},
},
});
var anotherCat = Object.create(cat);
anotherCat.say(); //meow~
var anotherTiger = Object.create(tiger);
anotherTiger.say(); //roar!
可以看到,这里并没有类的存在,却也实现了抽象和复用。(但这里需要指出,js 中不提倡让方法名相同,来重写,而是建议使用不同的名称,原因后述)
而如果我们要用类来模拟的话,在 es6 之前我们就绕不开混入模式,写出丑陋的显式伪多态/寄生继承的代码。见附录
模拟类设计模式-从 function 开始的模拟之路
如果你不喜欢追根溯源,那么这一章完全可以跳过,因为它真的很丑陋且毫无用处。
在看之前,抛弃大猫,接受猫科豹属豹亚种的想法
模拟-构造函数与 new
类实例是由一个特殊的类方法构造的,这个方法名通常和类名相同,被称为构造函数
但我们需要清楚,js 中的 new 只是一个语法糖,它并不是面向类的语言里的 new。
所谓的构造函数,只是让我们原有的函数进行了一个“构造调用”,或者说是在使用 new 操作符时被调用的函数
那么,当我们使用 new 的时候,发生了什么?
- 创建一个全新的对象
- 这个新对象会被执行[[Prototype]]链接
- 这个新对象会绑定到函数调用的 this(new 绑定)
- 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象
function Foo() {}
var a = new Foo();
console.log(Object.getPrototypeOf(a) === Foo.prototype); //true
这里着重强调的就是这个链接的过程。链接就让两个对象相互关联了起来,以实现类之间的关系
然而还没完,Foo.prototype 还有一招:
function Foo() {}
var a = new Foo();
console.log(a.constructor === Foo); //true
constructor 属性是哪来的?为什么它还是 Foo?
实际上,在代码第一行声明时,Foo.prototype 就存在一个公有且不可枚举的属性:constructor,这个属性引用的是对象关联的函数(Foo)
通过构造函数创建的对象(a),都会自动获取到这个属性,指向构造函数本身
第二句话可能不太好理解,我们打印 a 出来看看
其实意思就是,constructor 不是 a 的属性,只不过可以通过 a.constructor 来访问到它罢了,我们通过这种方式拿到的就是 Foo.prototype 上的 constructor,从始至终就只有一个玩意,就和我们前面通过anotherObj.b
来访问 b 的原理一样
看看这几句代码,应该能懂“指向构造函数本身”了吧
console.log(a.__proto__.constructor === Foo); //true 为什么用__proto__见上
a 和 constructor 一点关系都没有!a 是通过委托给 prototype 获取的 constructor!
function Foo() {}
Foo.prototype = {};
var a = new Foo();
console.log(a.constructor === Foo); //false
console.log(a.constructor === Object); //true
//如果想fix的话可以再给Foo.prototype添加一个constructor,且enumable:false,这里就不再赘述了
另外还要强调的一点就是刚才提到过的构造调用。调用,new 做完劫持后会(以构造对象的形式)调用这个函数
function NothingSpecial() {
console.log("Dont mind me!");
}
var a = new NothingSpecial();
//Dont mind me!
好的,终于铺垫完了,其实构造的代码就这么点
function Foo(name) {
this.name = name;
}
Foo.prototype.getName = function () {
return this.name;
};
var a = new Foo("a");
var b = new Foo("b");
console.log(a.getName());
console.log(b.getName());
当然,我们可以说 a 和 b 都是 Foo 的实例,并且通过 this 绑定实现了我们预期中的效果
插叙-手写 new 一道经典面试题
function myNew(constructor, ...args) {
//使用__proto__ 1、2步
// 创建一个空对象,它将成为新的实例
const instance = {};
// 将实例的原型指向构造函数的原型,建立原型链
if (constructor.prototype !== null) {
instance.__proto__ = constructor.prototype;
}
// 使用Object.create 12步合一起
// const instance = Object.create(constructor.prototype);
// 使用构造函数来初始化实例,将构造函数内部的属性和方法应用于实例(绑定this)
const result = constructor.apply(instance, args);
// 如果构造函数返回一个对象,那么返回该对象;否则返回实例对象
return typeof result === "object" ? result : instance;
}
模拟-继承与多态
//Animal类
function Animal(name) {
this.name = name;
}
//Animal类-speak方法
Animal.prototype.speak = function () {
console.log(this.name + " makes a noise");
};
Animal.prototype.jump = function () {
console.log(this.name + " jump");
};
function Cat(name) {
this.name = name;
}
//Cat与Animal建立父子关系
Cat.prototype = Object.create(Animal.prototype);
//重写父类speak方法
Cat.prototype.speak = function () {
console.log(this.name + " meow~");
};
var cat1 = new Cat("Tom1");
var cat2 = new Cat("Tom2");
cat1.speak(); //Tom1 meow~
cat2.speak(); //Tom2 meow~
cat1.jump(); //Tom1 jump
cat2.jump(); //Tom2 jump
可以看到,我们成功的模拟了 Animal-Cat-cat1/2 实例对象的关系,并且也实现了 Cat 重写 speak 方法,继承 jump 方法
而且找不出任何毛病
那不出意外的话肯定就要出意外了。
某一天,cat1 突然不想 speak 了,但是它采取了一种错误的修改方式
Object.getPrototypeOf(cat1).speak = function () {
console.log(this.name + " dont want to meow!");
};
cat1.speak(); //Tom1 dont want to meow!
cat2.speak(); //Tom2 dont want to meow!
由此可见,我们费劲心思的模拟还是有问题的:实例化对象之间,仍通过 prototype 关联着
尽管我们可以通过避免这种错误代码的编写来规避问题,但是本质始终如此
正确代码:
cat1.speak = function () {
console.log(this.name + " dont want to meow!");
};
还没完,有一天,cat1 想回归本能,想要调用 Animal 的 speak 方法,怎么办?
但是由于在 Cat 中重写了 speak,这个方法已经被覆盖了
那其实最简单的方式,就是不要重写,将 Animal 原型上的 speak 更名为 animalSpeak,这样我们在 cat1 中直接调,就会顺着原型链一路向前,最终找到并调用方法,且不用我们去操作 this 的指向
或者对 this 熟练的话,操作一下 this:Animal.prototype.speak.call(cat1);
,这样感觉比较类似 Animal::speak()
hh
差别
两种模式最大的差距则是在于复制操作上:
类设计模式的复制,是深拷贝,可以理解为是切实的复制了对象,从此两个对象再无关联。
原型设计模式的复制,是浅拷贝,新对象持有一个原型的引用。
说白了,无论用不用语法糖,js 的类设计始终是不完善的,你需要在清楚它的运行模式下进行
es6 语法糖-用 class 的方式模拟类
让我们继续刚才的例子,拿最经典的动物-猫狗的例子来说
C++中,我们会先创建 Animal 类,然后再创建 Cat/Dog 类 继承 Animal,并且重写 Animal 类中提供的方法。(多态)
得益于 es6 语法糖,我们可以通过相同的方式来模拟:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(this.name + " makes a noise");
}
jump() {
console.log(this.name + " jump");
}
}
class Dog extends Animal {
constructor(name) {
super(name);
}
speak() {
console.log(this.name + " wang!");
}
}
class Cat extends Animal {
constructor(name) {
super(name);
}
speak() {
console.log(this.name + " meow~");
}
}
const dog = new Dog("Mitzie");
const cat = new Cat("Tom");
//1 实例化对象调方法
dog.speak();
cat.speak();
dog.jump();
cat.jump();
//2 实例化对象和类的继承关系
console.log(dog instanceof Dog);
console.log(dog instanceof Animal);
console.log(dog instanceof Cat);
//3 子类的原型表现(这个才是js的实现)
console.log(Dog.prototype);
console.log(Cat.prototype);
console.log(Dog.prototype === Cat.prototype);
console.log(cat.jump === dog.jump);
不妨先思考一下这几组console.log
的输出,看看有没有不符合预期的地方
答案:
//1 实例化对象调方法
dog.speak(); //Mitzie wang!
cat.speak(); //Tom meow~
dog.jump(); //Mitzie jump
cat.jump(); //Tom jump
//2 实例化对象和类的继承关系
console.log(dog instanceof Dog); //true
console.log(dog instanceof Animal); //true
console.log(dog instanceof Cat); //false
//3 子类的原型表现(这个才是js的实现)
console.log(Dog.prototype); //Animal {}
console.log(Cat.prototype); //Animal {}
console.log(Dog.prototype === Cat.prototype); //false 每个构造函数都会创建自己的原型对象 结合new理解
console.log(cat.jump === dog.jump); //true
对于 1 ,熟悉重写的程序员会觉得很正常。而 js 中是这么描述的:
如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做 overriding
如果想要在 dog 上调 Animal 的 speak 方法,应该怎么做?
C++
的话可能会立刻想到Animal::speak()
而对于js
,我们只能通过在 Dog 类里新增方法:
animalSpeak(){ super.speak() }
所以,对于父类中可能会用到的方法,我们不使用重写,在子类/父类中直接换一个方法名反而是更简洁的措施。
但首先应该反思一下自己采取的模式是否正确,因为想要同时用到父类和子类的相同方法就更像是委托而非继承
不需要用到父类的方法当然是最好的,为了保持统一性,你想重写也完全没有问题,不过要注意只有ts才支持abstract关键字
而在 Java 和 C++中,方法重写是一种非常重要且被提倡的编程概念,它是面向对象编程的基础之一,用于实现多态性
对于 2,我们使用instanceof
判断对象是否为某个构造函数的实例
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof
instanceof
运算符的左边是实例对象,右边是构造函数。它会检查右边构建函数的原型对象(prototype),是否在左边对象的原型链上。
对于dog instanceof Dog
,Dog.prototype.isPrototypeOf(dog)
也是一种等价的写法
另外,
instanceof 运算符通常用于检查对象是否是特定构造函数的实例,而 isPrototypeOf 方法通常用于检查一个对象是否是另一个对象的原型。
通过console.log(dog instanceof Animal); //true
,我们也能得知instanceof
是遍历整条原型链的。
对于 3,我们研究子类的原型
这里最重要就在于最后一条cat.jump === dog.jump
是为true
的。说明 cat 和 dog 共享了同一个 jump 方法!
但上一条Dog.prototype === Cat.prototype // false
明明表明了二者的原型是新的 Animal 对象,为什么会这样呢?
让我们修改 jump 函数,来测试一下
修改实例化对象上的 jump:
dog.jump = () => {
console.log("The dog doesn't want to jump anymore");
};
dog.jump(); //The dog doesn't want to jump anymore
cat.jump(); //Tom jump
修改 Animal 原型上的 jump:
Animal.prototype.jump = () => {
console.log("I dont want to jump anymore!");
};
dog.jump(); //I dont want to jump anymore!
cat.jump(); //I dont want to jump anymore!
而第二种情况在实际开发中,无论是有意还是无意,修改到就会产生的坑。(不过你可以放心的绑定 this,因为 new 绑定的优先级高于显式绑定)
同时,这个例子也说明了:Cat 和 Dog 中的 jump 只是对于同一个函数的不同引用
同样的,class 语法糖没有解决刚才用 function 模拟的时候遇到的问题:实例化对象之间通过 prototype 有联系
const cat2 = new Cat("Tom2");
Object.getPrototypeOf(cat2).speak = function () {
console.log(this.name + " dont want to meow~");
};
cat.speak(); //Tom dont want to meow~
cat2.speak(); //Tom2 dont want to meow~
我们为什么会理所应当的觉得 jump 函数不同,正是因为被传统类设计思想误导了,认为通过 new 产生的新的 Animal 对象里有属于它的方法
再次重申:
js 中的所谓方法根本就不存在:一个类不会拥有一个函数,函数是作为对象的属性存在的,而不是类的一部分。
函数本身就是对象,和对象是并列的兄弟关系,只不过平时被人调用惯了。这也是 js 函数和 Java、C++这类面向对象语言最大的一点不同。
刚才例子中的 jump 函数,同时它也是一个对象。其他引用类型的数据的表现是一样的:复制的新对象拿到的只是引用,修改它会引起别的对象的变化!
另外,为什么在实例化对象上修改不会存在这个问题?根据 prototype 想一下就知道了,答案在后面揭晓^^
js 的 prototype 继承
js 的 prototype 机制,是所有继承的基石。但暴露 prototype,也意味着你随时可能破坏掉继承关系。
但这个破坏又非常讲究了。之前我们也提到过 instanceof
,他会向上遍历原型链,判断 instance 的原型链中是否包含 constructor 的原型对象。
让我们继续推进:
Cat.prototype.newFn = () => {
console.log("这是我在外部新添加的方法!");
};
//有个小细节是如果想拿到this就别用箭头函数
Cat.prototype.speak = function () {
console.log(this.name, "这是我在外部改变的speak方法!");
};
cat2.newFn(); //这是我在外部新添加的方法!
cat2.speak(); //Test 这是我在外部改变的speak方法!
//理清楚新加的/改变的在哪里
console.log(cat2.hasOwnProperty("newFn")); //false
console.log(Object.getPrototypeOf(cat2).hasOwnProperty("newFn")); //true
console.log(Cat.hasOwnProperty("newFn")); //false
console.log(Cat.prototype === Object.getPrototypeOf(cat2)); //true
//这种改变没有破坏继承关系 实例是子类实例,也是父类实例
console.log(cat2 instanceof Cat); //true
console.log(cat2 instanceof Animal); //true
//当一个类继承自另一个类时,子类的prototype对象会成为父类prototype对象上的一个实例
console.log(Cat.prototype instanceof Animal); //true
好了,现在让我们揭晓刚才问题的答案:我们只需要清楚一点:在 dog/cat 这个对象身上,到底有没有 jump 方法?
(对于 cat/cat2 同理)
答案是没有。我们能访问它是通过他的 prototype 拿到的。那在我们单独对 dog.jump 重新赋值后呢?
实际上,dog 这个实例对象的 jump 方法被我们重写了,之后 dog 就直接拥有了这个属性
而 cat 身上依旧没有,它往上找,一直到 Animal 的 prototype,找到了,于是拿来用
dog.speak = () => {
console.log("nonnondayo!");
};
console.log("check-dog", dog.hasOwnProperty("speak")); //true
这个讲的也是刚才的那一长串代码。
那其实你也能猜到接下来我要怎么破坏继承关系了
Object.setPrototypeOf(cat2, {});
console.log(Object.getPrototypeOf(cat2) === Object.prototype); //false
console.log(Object.getPrototypeOf(cat2) instanceof Object); //true
console.log(cat2.hasOwnProperty("speak")); //false
console.log(cat2 instanceof Cat); //false
console.log(cat2 instanceof Animal); //false
console.log(Cat.prototype instanceof Animal); //true
但是,这种方式太明显了,我想不会有人看到这个 set 会犯错的
那么如果是这种修改呢?
const cat3 = new Cat("Tom3");
cat3.__proto__ = {};
console.log(cat3 instanceof Cat); //false
Cat.prototype.__proto__ = {};
console.log(Cat.prototype instanceof Animal); //false
回到 pre-2,这样的修改你会中招吗?
提供一个替代方案,而这个方案的道理和第一个例子中继承关系不变的例子是一样的。
如果你学过 vue2,vue2 的响应式原理是基于 defineProperty 的,为了响应式重写了数组方法。
原来的 Array.push 等方法不会改变数组的地址,因此 vue 监测不到数组的变化,为了监测,重写的 push 方法会改变数组的地址,相当于拿了一个新的数组给原数组替换掉,以触发响应式更新
这个例子和我们这里对某个对象的属性动态的做了增删改,而对象的地址是没有变的是一个道理
const cat3 = new Cat("Tom3");
cat3.__proto__.newkey = 123;
console.log(cat3 instanceof Cat); //true
Cat.prototype.__proto__.newkey = 123;
console.log(Cat.prototype instanceof Animal); //true
当然,这些都是坏的代码风格,你应该始终使用 Object.setPrototypeOf()
的方式显式破坏继承关系
我们真的需要类吗-委托
终于结束了繁琐的模拟类之旅,让我们回归大猫的思维,用委托的方式来重构代码(当然这也只是一种模式,选择喜欢的风格即可)
首先,抛弃一切的继承关系,转为委托关系
然后,我们不要抽象出类的概念,直接着手于一个现成对象,就拿这只 Katty 猫开始!
现在,我们来抽象一些它的属性,比如 name 和 description,比如一个方法 speak(),这对于我们的猫来说目前已经足够,于是我们可以直接写出如下代码
var Kitty = {
name: "Kitty",
description: "A cute cat",
speak() {
console.log(this.name + " meow~");
},
};
Kitty.speak(); //Kitty meow~
但是有一天,我们在路口遇到了另外一只猫 Tom,我们发现这只猫和我们有的这只 Katty 猫有些相似,也有很多地方不一样,例如他不会说 Meow,而是会说 aowwww,他也并不可爱,我们需要用新的描述词来为他描述,同时,他还会很多技能。
var Tom = {
name: "Tom",
description: "A handsome cat",
skills: ["run", "jump", "fun"],
speak() {
console.log(this.name + " aowwwwww!");
},
};
Tom.speak(); //Tom aowwwwww!
而此时,我们发现,这两只猫都会 speak,并且都有 name 和 description,那么我们就可以通过委托的方式,把他们公有的属性委托给一个新的对象托管,然后在这两只猫分别要做什么事情的时候,它不直接调用自己的方法,而是去找到这个委托对象,然后调用这个委托对象的方法。
var myFriends = {
setName: function (name) {
this.name = name;
},
setDescription: function (des) {
this.description = des;
},
doSpeak: function (text) {
console.log(this.name + " " + text);
},
};
var Kitty = Object.create(myFriends);
Kitty.init = function (name, des) {
this.setName(name);
this.setDescription(des);
};
Kitty.speak = function (text) {
this.doSpeak(text);
};
Kitty.init("Kitty", "A cute cat");
Kitty.speak("meow~"); //Kitty meow~
console.log(Kitty); //{name: 'Kitty', description: 'A cute cat', init: ƒ, speak: ƒ}
var Tom = Object.create(myFriends);
Tom.skills = ["run", "jump", "fun"];
Tom.init = function () {
this.setName("Tom");
this.setDescription("A handsome cat");
};
Tom.speak = function () {
this.doSpeak("aowwwwww!");
};
Tom.init();
Tom.speak(); //Tom aowwwwww!
console.log(Tom); //skills: Array(3), name: 'Tom', description: 'A handsome cat', init: ƒ, speak: ƒ}
这就是基于委托的一个 demo。现在我的这两个朋友就可以通过 prototype 追溯到 myFriends 对象的方式,去设置他们的名字、描述和说话了。
但是你或许会觉得后续的 init 和 speak 很奇怪,而且感觉凭空复杂了好多。那其实 init 和 speak 只是我对于他们行为的描述,你完全可以直接通过 Tom 调 setName 方法等,都是可以的,这里只是为了表示灵活性,还有我喜欢这样写而已。然后注意 doSpeak 和 speak 的设计,委托倡导的就是不重名!
但是这个例子还没有完,要体现委托,我们还有事情要做
经过一段时间之后,我的朋友扩充了很多,而有的朋友非常好啊,愿意提供他的能力并且免费为我打工。
刚好 Tom 遇到了某些事情,想要借我这个会 dig 的朋友一用。我就可以直接让他去找我的朋友了
同时我也愿意提供场所,让我的朋友们一起 happy
var myFriends = {
setName: function (name) {
this.name = name;
},
setDescription: function (des) {
this.description = des;
},
doSpeak: function (text) {
console.log(this.name + " " + text);
},
//新增
dig: function () {
console.log("A good friend is helping " + this.name + " dig!");
},
play: function (...args) {
args.forEach((item) => {
console.log(item.name + " ");
});
console.log("lets have fun!");
},
};
var Kitty = Object.create(myFriends);
Kitty.init = function (name, des) {
this.setName(name);
this.setDescription(des);
};
Kitty.speak = function (text) {
this.doSpeak(text);
};
Kitty.init("Kitty", "A cute cat");
var Tom = Object.create(myFriends);
Tom.skills = ["run", "jump", "fun"];
Tom.init = function () {
this.setName("Tom");
this.setDescription("A handsome cat");
};
Tom.speak = function () {
this.doSpeak("aowwwwww!");
};
Tom.init();
Tom.dig(); //A good friend is helping Tom dig!
var me = Object.create(myFriends);
me.host = function (...args) {
this.play(...args);
};
me.host(Tom, Kitty);
//Tom
//Kitty
//lets have fun!
如果你愿意,这个故事可以一直延续下去。
我希望你也能接受这种风格的代码,而且这更贴合 js 的 prototype 机制
委托行为意味着某些对象在找不到属性或者方法引用时,会把这个请求委托给另一个对象
附录-其他模仿类方式
显式伪多态
// 非常简单的mixin(..)例子:
function mixin(sourceObj, targetObj) {
for (var key in sourceObj) {
// 只会在不存在的情况下复制
if (!(key in targetObj)) {
targetObj[key] = sourceObj[key];
}
}
return targetObj;
}
var Vehicle = {
engines: 1,
ignition: function () {
console.log("Turning on my engine.");
},
drive: function () {
this.ignition();
console.log("Steering and moving forward! ");
},
};
var Car = mixin(Vehicle, {
wheels: 4,
drive: function () {
//this改到子级,也就是兼具父子属性的Car上,虽然这个例子看不出效果,可以在ignition处log一下this
Vehicle.drive.call(this);
console.log("Rolling on all " + this.wheels + " wheels! ");
},
});
Car.drive();
这里主要可以说明一点,如果不用重写,我们混入就完全不用操作这个复杂的 this,因此推荐委托模式不要用重写!
同时这种方式也会共享引用类型的数据
寄生继承
// “传统的 JavaScript 类”Vehicle
function Vehicle() {
this.engines = 1;
}
Vehicle.prototype.ignition = function () {
console.log("Turning on my engine.");
};
Vehicle.prototype.drive = function () {
this.ignition();
console.log("Steering and moving forward! ");
};
// “寄生类” Car
function Car() {
// 首先,car 是一个 Vehicle
// 寄生的体现也在于这个new调用的位置
var car = new Vehicle();
// 接着我们对car进行定制
car.wheels = 4;
// 保存到Vehicle::drive()的特殊引用
var vehDrive = car.drive;
// 重写Vehicle::drive()
car.drive = function () {
vehDrive.call(this);
console.log("Rolling on all " + this.wheels + " wheels! ");
};
return car;
}
var myCar = new Car();
myCar.drive();
隐式混入
var Something = {
cool: function () {
this.greeting = "Hello World";
this.count = this.count ? this.count + 1 : 1;
this.obj = {
val: this.count ? this.count + 1 : 1,
};
console.log("计数器被调用了一次");
},
};
Something.cool();
console.log(Something.greeting);
// "Hello World"
console.log(Something.count); // 1
var Another = {
cool: function () {
console.log("this", this);
// 隐式把Something混入Another
Something.cool.call(this);
},
};
Another.cool();
console.log(Another.greeting);
// "Hello World"
console.log(Another.count); // 1(count不是共享状态)
console.log(Another.obj.val); //2 对象共享状态
不共享状态的原因很简单:count 是基本类型的数字
加了一个 obj 进去则可以共享