learn-tech/专栏/JavaScript进阶实战课/08深入理解继承、Delegation和组合.md
2024-10-16 06:37:41 +08:00

343 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

因收到Google相关通知网站将会择期关闭。相关通知内容
08 深入理解继承、Delegation和组合
你好,我是石川。
关于面向对象编程最著名的一本书就数GoFGang of Four写的《设计模式可复用面向对象软件的基础》了。这本书里一共提供了23种不同的设计模式不过今天我们不会去展开了解这些细节而是会把重点放在其中一个面向对象的核心思想上也就是组合优于继承。
在JS圈有不少继承和组合的争论。其实无论是继承还是组合我们都不能忘了要批判性地思考。批判性思考的核心不是批判而是通过深度思考核心问题让我们对事物能有自己的判断。
所以,无论是继承还是组合,都只是方式、方法,它们要解决的核心问题就是如何让代码更加容易复用。
那么接下来我们就根据这个思路看看JavaScript中是通过哪些方法来解决代码复用这个问题的以及在使用不同的方法时它们各自解决了什么问题、又引起了什么问题。这样我们在实际的业务场景中就知道如何判断和选择最适合的解决方式了。
继承
在传统的OOP里面我们通常会提到继承Inheritance和多态Polymorphism。继承是用来在父类的基础上创建一个子类来继承父类的属性和方法。多态则允许我们在子类里面调用父类的构建者并且覆盖父类里的方法。
那么下面我们就先来看下在JavaScript里要如何通过构建函数来做继承。
如何通过继承多态重用?
实际上从ES6开始我们就可以通过extends的方式来做继承。具体如下所示
class Widget {
appName = "核心微件";
getName () {
return this.appName;
}
}
class Calendar extends Widget {}
var calendar = new Calendar();
console.log(calendar.hasOwnProperty("appName")); // 返回 true
console.log(calendar.getName()); // 返回 "核心微件"
calendar.appName = "日历应用"
console.log(typeof calendar.getName); // 返回 function
console.log(calendar.getName()); // 返回 “日历应用”
接着来看多态。从ES6开始我们可以通过super在子类构建者里面调用父类的构建者并且覆盖父类里的属性。可以看到在下面的例子里我们是通过super将Calendar的appName属性从“核心微件”改成了“日历应用”。
class Widget {
constructor() {
this.appName = "核心微件";
}
getName () {
return this.appName;
}
}
class Calendar extends Widget {
constructor(){
super();
this.appName = "日历应用";
}
}
var calendar = new Calendar();
console.log(calendar.hasOwnProperty("appName")); // 返回 true
console.log(calendar.getName()); // 返回 "日历应用"
console.log(typeof calendar.getName); // 返回 function
console.log(calendar.getName()); // 返回 “日历应用”
在一些实际的例子如React这样的三方库里我们也经常可以看到一些继承的例子比如我们可以通过继承React.Component来创建一个WelcomeMessage的子类。
class WelcomeMessage extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
授权
说完了继承,我们再来看授权这个方法。
什么是授权Delegation我打个比方这里的授权不是我们理解的作为领导父类给下属子类授权而是作为个体对象可以授权给一个平台或者他人来一起做一件事。
就好像我和极客时间合作,我的个人精力和专业能力只允许我尽量做好内容,但是我没有精力和经验去做编辑、后期和推广等等,这时就授权给极客时间相关的老师来一起做,我在这件事、这个过程中只是专心把内容的部分做好。
如何通过授权做到重用?
在前面的例子中结合我们在第1讲里提到的基于原型链的继承我们会发现使用JavaScript无论是通过函数构建也好还是加了语法糖的类也好来模拟一般的面向对象语言比如Java的类和继承对于有些开发者来说是比较反直觉的。在使用的时候需要大量的思想转换才能把JavaScript的底层逻辑转换成实际呈现出来的实现。
那么有没有一种方式可以让代码更直观呢这种方式其实就是通过原型本身来做授权会更符合直觉。从ES5开始JavaScript就支持了Object.create()的方法。下面我们来看一个例子:
var Widget = {
setCity : function(City) {this.city = City; },
outputCity : function() {return this.city;}
};
var Weather = Object.create(Widget);
Weather.setWeather = function (City, Tempreture) {
this.setCity(City);
this.tempreture = Tempreture;
};
Weather.outputWeather = function() {
console.log(this.outputCity()+ ", " + this.tempreture);
}
var weatherApp1 = Object.create(Weather);
var weatherApp2 = Object.create(Weather);
weatherApp1.setWeather("北京","26度");
weatherApp2.setWeather("南京","28度");
weatherApp1.outputWeather(); // 北京, 26度
weatherApp2.outputWeather(); // 南京, 28度
可见我们创建的Weather天气预报这个对象授权给了Widget让Widget在得到授权的情况下帮助Weather来设定城市和返回城市。Widget对象在这里更像是一个平台它在得到Weather的授权后为Weather赋能。而Weather对象可以在这个基础上专注于实现自己的属性和方法并且产出weatherApp1和weatherApp2的实例。
当然也有开发者认为class的方式没有什么反直觉的那授权同样可以通过class来实现。比如我们如果想在上一讲提到过的集合Set和字典Map的基础上加上计数的功能可以通过继承Set来实现。但是我们也可以反之在把部分功能授权给Map的基础上自己专注实现一些类似Set的API接口。
class SetLikeMap {
// 初始化字典
constructor() { this.map = new Map(); }
// 自定义集合接口
count(key) { /*...*/ }
add(key) { /*...*/ }
delete(key) { /*...*/ }
// 迭代返回字典中的键
[Symbol.iterator]() { return this.map.keys(); }
// 部分功能授权给字典
keys() { return this.map.keys(); }
values() { return this.map.values(); }
entries() { return this.map.entries(); }
}
组合
说完了授权,我们再来看看组合。当然上面我们说的授权,广义上其实就是一种组合。但是这种组合更像是“个体和平台的合作”;而另一种组合更像是“团队内部的合作”,它也有很多的应用和实现方式,我们可以来了解一下。
如何通过借用做到重用?
在JavaScript中函数有自带的apply和call功能。我们可以通过apply或call来“借用”一个功能。这种方式也叫隐性混入Implicit mixin。比如在数组中有一个原生的slice的方法我们就可以通过call来借用这个原生方法。
如下代码示例我们就是通过借用这个功能把函数的实参当做数组来slice。
function argumentSlice() {
var args = [].slice.call(arguments, 1, 3);
return args;
}
// example
argumentSlice(1, 2, 3, 4, 5, 6); // returns [2,3]
如何通过拷贝赋予重用?
除了“借力”以外我们还能通过什么组合方式来替代继承呢这就要说到“拷贝”了。这个方法顾名思义就是把别人的属性和方法拷贝到自己的身上。这种方式也叫显性混入Explicit mixin
在ES6之前人们通常要偷偷摸摸地“抄袭”。在ES6之后JavaScript里才增加了“赋予”也就是Object.assign()的功能,从而可以名正言顺地当做是某个对象“赋予”给另外一个对象它的“特质和能力”。
那么下面我们就先看看在ES6之后JavaScript是如何名正言顺地来做拷贝的。
首先通过对象自带的assign()我们可以把Widget的属性赋予calendar当然在calendar里我们也可以保存自己本身的属性。和借用一样借用和赋予都不会产生原型链。如以下代码所示
var widget = {
appName : "核心微件"
}
var calendar = Object.assign({
appVersion: "1.0.9"
}, widget);
console.log(calendar.hasOwnProperty("appName")); // 返回 true
console.log(calendar.appName); // 返回 “核心微件”
console.log(calendar.hasOwnProperty("appVersion")); // 返回 true
console.log(calendar.appVersion); // 返回 “1.0.9”
接着我们再来看看在ES6之前人们是怎么通过“抄袭”来拷贝的。
这里实际上分为“浅度拷贝”和“深度拷贝”两个概念。“浅度拷贝”类似于上面提到的赋予assign这个方法它所做的就是遍历父类里面的属性然后拷贝到子类。我们可以通过JavaScript中专有的for in循环来遍历对象中的属性。
细心的同学可能会发现我们在第2讲中说到用拷贝来做到不可变时就了解过通过延展操作符来实现浅拷贝的方法了。
// 数组浅拷贝
var a = [ 1, 2 ];
var b = [ ...a ];
b.push( 3 );
a; // [1,2]
b; // [1,2,3]
// 对象浅拷贝
var o = {
x: 1,
y: 2
};
var p = { ...o };
p.y = 3;
o.y; // 2
p.y; // 3
而在延展操作符出现之前人们大概可以通过这样一个for in循环做到类似的浅拷贝。
function shallowCopy(parent, child) {
var i;
child = child || {};
for (i in parent) {
if (parent.hasOwnProperty(i)) {
child[i] = parent[i];
}
}
return child;
}
至于深度拷贝,是指当一个对象里面存在嵌入的对象就会深入遍历。但这样会引起一个问题:如果这个对象有多层嵌套的话,是每一层都要遍历吗?究竟多深算深?还有就是如果一个对象也引用了其它对象的属性,我们要不要也拷贝过来?
所以相对于深度拷贝浅度拷贝的问题会少一些。但是在第2讲的留言互动区我们也说过如果我们想要保证一个对象的深度不可变还是需要深度拷贝的。深度拷贝的一个相对简单的实现方案是用JSON.stringify。当然这个方案的前提是这个对象必须是JSON-safe的。
function deepCopy(o) { return JSON.parse(JSON.stringify(o)); }
同时在第2讲的留言区中也有同学提到过另外一种递归的实现方式所以我们也大致可以通过这样一个递归来实现
function deepCopy(parent, child) {
var i,
toStr = Object.prototype.toString,
astr = "[object Array]";
child = child || {};
for (i in parent) {
if (parent.hasOwnProperty(i)) {
if (typeof parent[i] === "object") {
child[i] = (toStr.call(parent[i]) === astr) ? [] : {};
deepCopy(parent[i], child[i]);
} else {
child[i] = parent[i];
}
}
}
return child;
}
如何通过组合做到重用?
上面我们说的无论是借用、赋予深度还是浅度拷贝都是一对一的关系。最后我们再来看看如何通过ES6当中的assign来做到组合混入也就是说把几个对象的属性都混入在一起。其实方法很简单以下是参考
var touchScreen = {
hasTouchScreen : () => true
};
var button = {
hasButton: () => true
};
var speaker = {
hasSpeaker: () => true
};
const Phone = Object.assign({}, touchScreen, button, speaker);
console.log(
hasTouchScreen: ${ Phone.hasChocolate() }
hasButton: ${ Phone.hasCaramelSwirl() }
hasSpeaker: ${ Phone.hasPecans() }
);
React中的组合优于继承
在React当中我们也可以看到组合优于继承的无处不在并且它同样体现在我们前面讲过的两个方面一个是“团队内部的合作”另一个是“个体与平台合作”。下面我们先看看“团队内部的合作”的例子在下面的例子里WelcomeDialog就是嵌入在FancyBorder中的一个团队成员。
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
);
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
另外我们也可以看到“个体与平台合作”的影子。在这里WelcomeDialog是一个“专业”的Dialog它授权给Dialog这个平台借助平台的功能实现自己的title和message。这里就是用到了组合。
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
</FancyBorder>
);
}
function WelcomeDialog() {
return (
<Dialog
title="Welcome"
message="Thank you for visiting our spacecraft!" />
);
}
总结
这节课我们了解了通过JavaScript做到代码复用的几种核心思想和方法从传统的继承到JavaScript特色的授权以及组合等方式都有分析。虽然我说授权和组合优于继承但实际上它们之间的关系不是非黑即白的。
我们看到在前端圈有很多大佬比如道格拉斯·克罗克福德Douglas Crockford和凯尔·辛普森Kyle Simpson都是基于授权的对象创建的积极拥护者而像阿克塞尔·劳施迈尔博士Dr. Axel Rauschmayer则是基于类的对象构建的捍卫者。
我们作为程序员,如果对对象和面向对象的理解不深入,可能很容易在不同的论战和观点面前左摇右摆。而实际的情况是,真理本来就不止一个。我们要的“真理”,只不过是通过一个观察角度,形成的一个观点。这样,才能分析哪种方式适合我们当下要解决的问题。这个方式,只有在当下,才是“真理”。而我们通过这个单元整理的方法,目的就是帮助我们做到这样的观测。
思考题
在前面一讲中我们试着通过去掉对象私有属性的语法糖来看如何用更底层的语言能力来实现类似的功能。那么今天你能尝试着实现下JS中的类和继承中的super以及原型和授权中的Object.create()吗?
欢迎在留言区分享你的答案、交流学习心得或者提出问题,如果觉得有收获,也欢迎你把今天的内容分享给更多的朋友。我们下节课见!