看到原型模式,不知道大家有没有想起来 JS 的基石:原型及原型链。对我而言,第一次看到设计模式中竟然有原型模式时,我的第一反应就是好奇这是不是就是 JS 中原型呢,这个答案容我下先卖个关子,如果有兴趣,请先接着往下看:
在 JAVA 这类强类型语言的描述下,原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这样的形容对于 JAVA 中的原型来讲是没错的,但是用来形容 JavaScript 中的原型却是远远不够的,在 JS 中,原型的重要性可以说是重中之重。
需要说明的是,本系列的设计模式是从前端角度出发,因此更多的关于 JAVA 语言体系下的原型模式可以查阅其他文章。在本文中,着重于从 JS 的角度去理解原型模式。
大家可能会好奇,不是讲原型吗,怎么突然讲到 class 了?
正是大家有这样的疑问,因此在这里就很有必要提出来,请先看一下这两种写法:
1、使用 class 定义一个 Person 类:
class Person {
constructor(name, age, phone) {
this.name = name;
this.age = age;
this.phone = phone;
}
// 默认为 void,表示无返回值
public doEat(other, address) {
console.log(`${this.name}和${other}在${address}吃饭!`);
}
};
2、使用构造函数定义一个 Person 类:
var Person = (function () {
function Person(name, age, phone) {
this.name = name;
this.age = age;
this.phone = phone;
}
Person.prototype.doEat = function (other, address) {
console.log(`${this.name}和${other}在${address}吃饭!`);
};
return Person;
}());
实际上,这两种写法是一样的。因为后者实际上是前者编译后的结果(做了一点调整,是方便大家阅读)
因此,从原型继承的角度上来讲,class 就是原型继承的语法糖。
注:上面提到,JS 中的 class 是语法糖,这个说法不完全正确,不过在此我们不纠结这一点,如果有兴趣,可以查阅《从头再学 JavaScript 系列》中关于 Class 的内容。
那么关于 JS 中 Class 就简单提一下,后面基本上会用构造器的方式来分析原型模式,因此对于 Class 不熟悉的同学也不用担心。
我们仍然用上面提到的 Person 构造函数来举例。
1、首先,构建一个 Person 的构造函数
function Person(name, age, phone) {
this.name = name;
this.age = age;
this.phone = phone;
}
Person.prototype.doEat = function (other, address) {
console.log(`${this.name}和${other}在${address}吃饭!`);
};
2、通过 Person 创建一个实例
const zhangsan = new Person("张三", 24, "13911111111")
console.log(zhangsan);
那么 person 是什么样的呢?请看下图
可以看到,我们在 Person 的原型对象上绑定的 doEat 方法并没有出现在实例 zhangsan 中,那么我们能不能通过 zhangsan 来调用 doEat 方法呢?
让我们来试一试:
zhangsan.doEat("罗翔", "朝阳区"); // 张三和罗翔在朝阳区吃饭!
可以看到,虽然 doEat 方法没有出现在 zhangsan 这个实例中,但是却仍然可以使用到。这正是原型链的功劳,在上图中我们看到存在 [[Prototype]] 这个属性,让我们点开看看:
可以看到,doEat 方法竟然在 [[Prototype]] 这个属性中,这是为什么?
如果你了解原型及原型链,那么就会清楚, [[Prototype]] 这个属性指向的是正是构造函数的原型对象:prototype。
现在让我们直接看看构造函数 Person 的原型对象 prototype 吧:
console.log(Person.prototype);
果然上面提到的实例 zhangsan 的 [[Prototype]] 是指向其构造函数的原型对象(prototype)的。
好了,经过上面对原型的简单介绍,相信你会有一个初步的理解。不过由于关于原型的内容众多,仅仅这么几句话是不容易讲明白的,如果你有兴趣,可以读一读《从头再学 JavaScript 系列》中关于原型的部分。
在 JS 中,拷贝分为深拷贝与浅拷贝,简单来说,如果是基本类型则通过等号可以拷贝出来一个新的值,不过如果是引用类型的值,那么实际上你拷贝的仅仅是地址,而这个地址指向的值才是真正的数据。
文章一开始我们提到,原型模式(Prototype Pattern)是用于创建重复的对象,那么在 JS 中我们怎么实现深拷贝呢?
下面给出一个完整的深拷贝方案:
const cloneDeep = (value) => {
// 非数组和非对象直接返回值即可
if (value == null || typeof value !== 'object') {
return value;
}
// 初始化
let result = Array.isArray(value) ? [] : {};
for (let key in value) {
if (value.hasOwnProperty(key)) {
result[key] = cloneDeep(value[key]);
}
}
return result;
}
在这上面基本实现了对象和数组的深拷贝,更加详细的关于浅拷贝与深拷贝的内容可以查阅:https://blog.csdn.net/qq_40228484/article/details/118379290
文章最后,我们回答一下最开始提出的疑问:前端角度下的原型模式是不是就是 JS 中原型呢?
实际上,在 JS 的语言基础下,你可以这样理解,只不过 JS 中的原型更强大。在 JS 中谈原型模式,只需要你清楚的明白JS 中的原型及原型链的相关概念即可。
在 JS 中,原型就是用来定义对象和继承的基础。而在其他语言中,原型的重要性就不会有在 JS 中这么重,掌握好原型及原型链是学好 JS 的关键一步。建议阅读《从头再学 JavaScript 系列》中关于原型的部分,相信你一定能掌握好原型。