实验介绍
本实验主要为大家介绍了前端中原型模式,为了加深大家对原型的了解,实验中花费大量篇幅讲解了原型及原型的概念,并配上了相关的例子以帮助大家学习。随后我们对 class 进行了简单的介绍,它可以被简单的认为是语法糖。最后,为了帮助大家理解原型中的克隆,实验也对浅拷贝与深拷贝进行了介绍。
知识点
- 原型模式介绍
- 原型及原型链
- Class(类)
- 浅拷贝与深拷贝
- 实现挑战
原型模式介绍
原型模式定义:是用于创建重复的对象,同时又能保证性能。
JavaScript 的原型模式和其他静态语言的原型模式的表现形式是不太一样的,例如 java 中的原型模式:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
简单来讲,就是在 java 的类中,存在一个 clone 方法,通过这个 clone 方法能够快速的复制现有实例从而快速生成一个新实例。
这里为了不偏离本实验的主题,Java 中的原型模式例子可以参考:Java 原型模式 ,有 Java 基础的同学可以选择自行对照学习。
当然了,如果没有 Java 基础也无需担心,只要认真学习下面的内容,仍然能完全掌握前端层面的原型模式。
言归正传,接下来我们需要把全部注意力放在前端层面的原型模式上。为了更好的引出原型模式,在下面的小节中,会首先为大家介绍原型与原型链。这块内容理解起来会比较困难,希望大家能反复的学习,以求真正掌握。
原型及原型链
原型
在 JavaScript 中,我们首先明确两个概念,
- 对象有一个特殊的隐藏属性
[[Prototype]]
- 可以为函数添加一个名为 "prototype" 的常规属性,例如
Fn.prototype
现在无需你理解这两点,因在后面的内容中会对它们进行详细的讲解,不断地为你巩固这两个概念。
首先,[[Prototype]]
要么为 null,要么就是对另一个对象( 被称为 “ 原型 ” )的引用,就像这样:
现在来看一个具体的例子。
首先我们需要新建一个 index.html
和 prototype.js
文件,并引入,就像这样:
然后在 prototype.js
中添加如下代码:
// prototype.js
// 动物对象
const animal = {
eat: true,
};
// 兔子对象
const rabbit = {
jump: true,
};
// 为 rabbit 添加原型( animal )
rabbit.__proto__ = animal;
console.log(rabbit.eat); // true
在上述代码中,出现了 __proto__
这个属性。注意 proto
的前后都是两个下划线,这点很容易被忽略。
注:在这里你可以先简单的认为,__proto__
就是 [[Prototype]]
。但是这样说并不完全正确,不过这里你可以先这样理解,后面会专门为此做出解释。这里还是让我们回归上面的代码。
可以看到,我们通过 __proto__
为 rabbit
添加了原型 animal
。随后在打印输出中,未在 rabbit
中定义的 eat
属性却成功的打印了 true
。这实际上就是原型的功劳。
重点在于:当我们读取对象的某个属性时,如果在这个对象中并没有定义该属性,那么它就会主动的去该对象的原型中寻找是否有该属性。
我们可以尝试访问一下 rabbit
和它原型中都没有定义的属性,例如 see
,结果只能是 undefined
,就像这样:
// prototype.js
console.log(rabbit.see); // undefined
如上,rabbit
和 animal
就构成了一个简单的原型关系。通过上面的例子相信大家对于原型有了一定的了解。
现在请回忆下我们一开始提到的第一个概念:对象有一个特殊的隐藏属性 [[Prototype]]
。我们正是通过它来定义原型关系的: [[Prototype]]
是对另一个对象的引用,而这个对象就被称作 “ 原型 ” 。
原型链
上面提到的第二个概念:可以为函数添加一个名为 "prototype" 的常规属性,例如 Fn.prototype
。
有这样一句话,存在即是合理。为什么我们需要为函数添加一个 prototype
属性,必然是有意义的。
例如:在 javascript 中,有一个内置函数 Object
,然后它本身具有这个 prototype
属性,我们可以打印出来看一下:( Object.prototype
)
在图片中,可以清晰的看到 prototype
实际上就是一个对象,其中也有一个 __proto__
,它的值是 null
。这就是我们一开始说的 [[Prototype]]
为 null
的情况。
现在我们通过 Object
函数 new 一个 animal
对象:
// prototype.js
const animal = new Object();
console.log(animal);
打印结果是这样的:
展开 animal
对象会发现它仍然有一个 [[Prototype]]
属性,那么通过 new
的方式实例出来的对象,它的原型是指向哪里的呢?我们可以展开它看看:
可以看到,通过 new
的方式实例出来的 animal
,它的原型正是:Object.prototype
,也就是 animal
的构造函数的 prototype
属性对象。
而 Object.prototype
本身的原型为 null,表示在 Object
上面就不存在其他原型了。
同样,我们按一开始的方式,为 animal
添加 eat
属性,并把它设为 rabbit
的原型,就像这样:
// prototype.js
const animal = new Object();
animal.eat = true;
const rabbit = {
jump: true,
};
rabbit.__proto__ = animal;
console.log(rabbit.eat); // true
这一点与我们最上面的例子中并无区别,表示使用 {}
的方式书写对象和使用 new Object
的方式是一样的,前者是一种简写形式。
同学们可以打印一下 rabbit
对象,然后按上面的方式展开 [[Prototype]]
属性,你会发现:rabbit
的原型是 animal
,animal
的原型是 Object.prototype
,这便构成了一条 “原型链” 。
我们可以通过下面的方式进行验证:
// prototype.js
console.log(rabbit.__proto__ === animal); // true
console.log(animal.__proto__ === Object.prototype); // true
这里需要再次提醒大家几点:
- 只有对象才有原型
[[Prototype]]
- 函数可以添加一个
prototype
属性 prototype
本身是一个对象,所以它有[[Prototype]]
- 原型链可以很长
特别要指出的是:一个对象的原型要么是 null
,要么就是对另一个对象的引用。是不可能出现原型是函数的情况。
要清楚,Object
是内置函数,而 Object.prototype
才是对象,因此 animal
的原型只能是 Object.prototype
,而不是 Object
。
注:__proto__
是 [[Prototype]]
的因历史原因而留下来的 getter/setter
我们上面提到可以简单的认为 __proto__
就是 [[Prototype]]
,但是这样说并不是正确的。
请注意,__proto__
与内部的 [[Prototype]]
不一样。__proto__
是 [[Prototype]]
的 getter/setter
。
__proto__
属性有点过时了。它的存在是出于历史的原因,现代编程语言建议我们应该使用函数 Object.getPrototypeOf/Object.setPrototypeOf
来取代 __proto__
去 get/set
原型。
就像上面我们为 rabbit
添加了原型 animal
,也可以使用这种方式:
const animal = {
eat: true,
};
const rabbit = {
jump: true,
};
Object.setPrototypeOf(rabbit, animal);
console.log(rabbit.eat); // true
根据规范,__proto__
必须仅受浏览器环境的支持。但实际上,包括服务端在内的所有环境都支持它,因此我们使用它是非常安全的。
一般的情况下,由于 __proto__
标记在观感上更加明显,所以我们会在示例中使用这种方式。
内建原型
内建原型:Object、Array、Date、Function 等内建函数上的原型。
按照规范,所有的内建原型顶端都是 Object.prototype。这就是为什么有人说 “一切都从对象继承而来” 。
下面给出 3 个内建对象完整的示意图供参考:
从图上也可以看到,原型由下向上追溯,直至 Object.prototype
的原型 null
。
对于原型,大家可以参考这篇文章:继承与原型链
原型模式在前端中的表现
这里我们仍然会使用 ES5 中定义构造函数的方式,为大家展示 JavaScript 中原型的具体表现。
这里新建一个 person.js
文件,引入 index.html
中。
首先,构建一个 Person 的构造函数:
// person.js
function Person(name, age, phone) {
this.name = name;
this.age = age;
this.phone = phone;
}
随后在构造函数的 prototype
上添加一个 doEat
方法:
// person.js
Person.prototype.doEat = function (other, address) {
console.log(`${this.name}和${other}在${address}吃饭!`);
};
现在就可以通过 Person
创建一个实例:
// person.js
const zhangsan = new Person("张三", 24, "13911111111");
console.log(zhangsan);
那么 person
是什么样的呢?请看下图
可以看到,我们在 Person
的原型对象上绑定的 doEat
方法并没有出现在实例 zhangsan
中,但是我们可以通过 zhangsan
来调用 doEat 方法:
// person.js
zhangsan.doEat("罗翔", "朝阳区"); // 张三和罗翔在朝阳区吃饭!
在输出中展开原型,可以看到 doEat
正好挂在了 zhangsan
的原型上,也就是 Person.prototype
上:
这里给出整个 zhangsan
的原型链:
这就是原型的作用,相信通过这个小例子,大家可以能更好的了解原型。
而从这一点实际上大家可以看到,JS 中的原型不正是帮助我们在创建大量实例对象的时候,提升效率。我们并没有把一些公用方法写在实例中,但是实例化后的对象却可以顺利的调用到这些方法,这一点正好是原型模式在前端中的应用。
在上面我们反复的提到了原型与原型链,而实际上并未特别的对原型模式进行描述,这是因为在前端中,你完全可以将二者等同起来
接下来,我们会用 class 的方式再次实现这个例子,让大家了解下 ES6 中 class 和 原型的关系。
Class(类)
在 ES6 中,有一个高级的“类(class)”构造方式,它有许多非常棒的新功能,这些功能对于面向对象编程很有用。
新建一个 class.js
文件,引入 index.html
中。
基本语法是:
class MyClass {
// 构造函数
constructor() { ... }
// class 方法
method1() { ... }
method2() { ... }
method3() { ... }
...
}
那么 class 到底是什么,它本质上是一个函数:
// class.js
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
alert(this.name);
}
}
console.log(typeof Person); // function
class 更确切地说,是 constructor 方法:
// class.js
console.log(Person === Person.prototype.constructor); // true
大部分文章会说,class
是一个语法糖。这样说在某种程度上没错,但是这样并不是完全准确的。
首先要明白什么是语法糖:为了使内容更易阅读和更方便表示,但不引入任何新内容的语法。
而 class
却并不是这样,它除了让原型继承实现的更加简单直接以外,还和原本的原型方式之间存在着差异。
不过在本实验中,为了不偏离主题,你可以简单的认为 class
就是语法糖,这并不影响下面的示例。
使用 class
定义一个 Person
类:
// class.js
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}吃饭!`);
}
};
转成 ES5 的语法:
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
编译后的结果( 删除了一些注释,方便大家阅读 )。
可以看到,我们在 class
中写的方法经过转换后,实际上也是挂在 Person.prototype
上的。从原型继承的角度上来讲,说 class
是原型继承的语法糖是没问题的。
浅拷贝与深拷贝
在 JS 中,拷贝分为深拷贝与浅拷贝,简单来说,如果是基本类型则通过等号可以拷贝出来一个新的值,不过如果是引用类型的值,那么实际上你拷贝的仅仅是地址,而这个地址指向的值才是真正的数据。
文章一开始我们提到,原型模式(Prototype Pattern)是用于创建重复的对象,虽然在 JS 中的原型模式并不完全是这样,但是针对创建重复对象,即 clone 的行为,也可以来看一下在 JS 中实现深拷贝的方式。
这里直接给出一个完整的深拷贝方案:
新建一个 clone.js
文件,引入 index.html
中。
// clone.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;
};
接下来定义一个覆盖比较全面的变量用于检测该方法的克隆能力:
// clone.js
const arr = [
[1, 2, 3],
{ a: 4, b: 5 },
null,
undefined,
true,
0,
function () {
console.log("function");
},
Symbol("Symbol"),
];
const obj = {
a: [1, 2, 3],
b: { c: 4, d: 5 },
e: null,
f: undefined,
g: true,
h: 0,
i: function () {
console.log("function");
},
j: Symbol("Symbol"),
};
大家可以自行测验,这里也给出几个测试示例:
// clone.js
const newArr = cloneDeep(arr);
// 克隆不再相等
console.log(arr[0] === newArr[0]); // false
// 修改原本的数组,并不会影响到新克隆出来的数组
arr[1].a = 400;
console.log(arr[1].a, newArr[1].a); // 400 4
关于 JS 中的浅拷贝和深拷贝就简单介绍到这。如果想了解更多可以参考该文章:JS 浅拷贝与深拷贝
至此,对原型模式的介绍和相关的一些知识点就基本介绍完了。原型模式和前面提到的单例模式不一样,单例模式无论是在 Java 这类静态语言,还是 JavaScript 这种动态语言中,其实现上的基本思路是一致的。
而对于原型模式则不同,在 JavaScript 中原型的重要性是不可替代的,而原型模式在前端中的表现正是对原型和原型链的应用。而非生硬的去实现一个克隆(拷贝)。
如果大家想了解静态语言中的原型模式,可以参考:Java 原型模式
实现挑战
给定以下对象:
let head = {
glasses: 1,
};
let table = {
pen: 3,
};
let bed = {
sheet: 1,
pillow: 2,
};
let pockets = {
money: 2000,
};
- 使用
__proto__
来分配原型,以使得任何属性的查找都遵循以下路径:pockets → bed → table → head
。例如,pockets.pen
应该是 3(在 table 中找到),bed.glasses
应该是 1(在 head 中找到)。 - 回答问题:通过 pockets.glasses 或 head.glasses 获取 glasses,哪个更快?必要时需要进行基准测试。
答案代码放在实验最后课程的源码包里,大家可以自行下载。
实验总结
实验中花费了大量的篇幅为大家介绍原型和原型链,这不仅是 JS 的基石,也是我们理解前端中原型模式的一个重要前提。随后我们为大家介绍了一些其他相关的知识点,例如 class 和拷贝,都是为了让大家有一个更深的认识。相信通过本实验的学习,能为大家打好一个基础。
本节实验源码压缩包下载链接:原型模式源码
标签:__,设计模式,创建,Object,原型,animal,prototype,class From: https://www.cnblogs.com/xzemt/p/18030130