@[TOC] 一文彻底搞懂原型链
1. 实现new操作符
实现new操作符
该函数会返回一个对象,该对象的构造函数为函数参数、原型对象为函数参数的原型,核心步骤有:1. 创建一个空对象,作为将要返回的对象实例 2. 将这个空对象的原型,指向了构造函数的prototype属性 3. 将这个空对象赋值给函数内部的this关键字 4. 开始执行构造函数内部的代码 5. 如果构造函数返回一个对象,那么就直接返回该对象,否则返回创建的对象
function newObj(fn, ...rest) {
let obj = {};
obj.__proto__ = fn.prototype;
fn.apply(obj, rest);
return obj;
}
1. Object.create
该函数创建一个新对象,使用现有的对象来提供新创建的对象的proto,核心步骤有:
1. 创建一个临时函数 2. 将该临时函数的原型指向对象参数 3. 返回该临时函数的实例
Object.prototype._objectCreate = function(prototype) {
function F() {}
F.prototype = prototype;
return new F();
}
注意:
Object.create() 方法将原型式继承的概念规范化
new 是隐式原型继承,Object.create 是显式原型继承
常会看到 Object.create(null) ,用此初始化一个新对象,至于为什么用这个方法而不用 new Object 或者 {},是因为无论 new 还是字面量,都是继承自 Object 构造函数,而使用Object.create(null) ,能得到一个没有任何继承痕迹的对象
2. 浅拷贝
实现浅拷贝
浅拷贝适用于一些简单的对象,不需要考虑深层次的属性或者循环引用的问题。核心步骤有:1. 首先,需要检查传入的参数是否是一个对象,如果不是,直接返回该参数。 2. 然后,创建一个空对象,用来存储拷贝后的新对象。 3. 接着,遍历原对象的所有自有属性(不包括继承的属性),并使用 Object.assign () 方法或者赋值运算符,将它们复制到新对象中。 4. 最后,返回新对象。
const _shallowClone = target => {
// 检查target是否是对象
if(typeof target !== "object" || target === null) return target;
// 创建一个空对象
let newObj = {};
// 遍历target的所有自由属性
for(let key in target) {
if(target.hasOwnProperty(key)) { // 判断属性是否属于target自身
Object.assign(newObj, { [key]: target[key] }); // 使用Object.assign方法或者赋值运算符,将属性复制到newObj
// newObj[key] = target[key];
}
}
return newObj;
}
浅拷贝缺点: 只能复制对象的第一层属性,如果对象的属性值还是对象,那么它们不会被复制,而是共享同一个引用。 如果想要实现深度拷贝的功能,需要使用递归的方式,对每个属性值都进行拷贝操作。
这种方法可能无法正确地复制一些特殊类型的属性值,比如函数、正则、日期、ES6 新对象等。如果想要实现更完善的拷贝功能,需要使用一些特殊的处理方法,比如使用 **toString () 或者 toJSON () **来转换属性值。
3. 简易深拷贝
只考虑基本数据、数组和普通对象类型, 无需考虑循环引用问题。利用遍历、递归:
- 如果对象参数的数据类型不为“object”或为“null”,则直接返回该参数
- 根据该参数的数据类型是否为数组创建新对象
- 遍历该对象参数,将每一项递归调用该函数本身的返回值赋给新对象
const _sampleDeepClone = target => {
if (typeof target !== 'object' || target === null) return target;
const cloneTarget = target instanceof Array ? [] : {};
for(let k in target) {
if (target.hasOwnProperty(k))
Object.assign(cloneTarget, {[k]: _sampleDeepClone(target[k])})
// cloneTarget[k] = _sampleDeepClone(target[k]);
}
return cloneTarget;
}
4. 深拷贝
需要考虑函数、正则、日期、ES6新对象 ; 需要考虑循环引用问题。
考点:递归、遍历、Map【循环引用】实现核心步骤: 1. 首先判断对象参数是否为“null”或者“object”,不是则返回该参数; 2. **获取到对象参数的构造函数名**,判断是否为函数、正则、日期、ES6新对象其中之一,如果是则直接返回通过该参数对象对应的构造函数生成的新实例对象 3. 当以上条件判断之后函数依然没有结束时继续进行以下操作 4. 在Map对象中获取当前参数对象,如果能获取到,则说明这里为循环引用并返回Map对象中该参数对象的值 如果在Map对象中没有获取到对应的值,则保存该参数对象到Map中,作为标记; 5. 根据该参数的数据类型是否为数组创建新对象 6. 遍历该对象参数,将每一项递归调用该函数本身的返回值赋给新对象
const _completeDeepClone = (target, map = new Map()) => {
if (target === null || typeof target !== "object") return target;
const fn = target.constructor;
const res = /^(Fuction|RegExp|Date|Map|Set)$/i;
if (res.test(fn.name)) return new fn(target);
if (map.get(target)) return map.get(target);
map.set(target, true);
const cloneTarget = Array.isArray(target) ? [] : {};
for(let k in target) {
if(target.hasOwnProperty(k)) {
cloneTarget[k] = _completeDeepClone(target[k], map)
}
}
return cloneTarget;
}
5. 事件委托
事件委托是一种在JavaScript中处理事件的技术。它利用了事件的冒泡机制,将事件处理程序绑定到它们的共同祖先元素上,而不是直接绑定到每个子元素上。当事件触发时,事件会从子元素一直冒泡到祖先元素,然后通过判断事件的目标元素来执行相应的事件处理程序。
优势:
- 内存效率:通过减少事件处理程序的数量,节省内存资源。相比每个子元素都绑定事件处理程序,只需在共同祖先元素上绑定一个事件处理程序即可。
- 动态处理:当动态添加或移除子元素时,无需重新绑定和解绑事件处理程序。因为事件处理程序是绑定到祖先元素上的,不受子元素的变化影响。
- 简化代码:可以减少重复的事件绑定代码,简化代码结构。尤其是当有大量子元素需要绑定相同的事件处理程序时,使用事件委托可以显著简化代码。
- 动态事件处理:通过判断事件的目标元素,可以根据需要选择执行不同的操作或处理程序。这样可以更灵活地处理事件,并且不需要为每个子元素都编写独立的事件处理程序。
缺点:
- 不适用于所有场景:某些需要特定处理的事件,仍需要直接绑定到子元素上,而不适用于委托给祖先元素处理。
- 目标元素判断:在事件处理程序中需要正确判断事件的目标元素,以执行相应的操作。如果目标元素判断逻辑复杂或错误,可能会导致意外行为。
- 不支持所有事件:某些特定的事件(例如focus、blur等)无法在祖先元素上进行委托。
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
/* 填写样式 */
</style>
</head>
<body>
<ul>
<li>.</li>
<li>.</li>
<li>.</li>
</ul>
<!-- 填写标签 -->
<script type="text/javascript">
// target表示当前触发事件的元素
// currentTarget是绑定处理函数的元素
// innerText 可修改,nodeValue不可修改
document.querySelector('ul').onclick = event => {
event.target.innerText += '.';
}
</script>
</body>
</html>
6. 发布订阅模式
观察者模式有一个别名叫“发布-订阅模式”,或者说是“订阅-发布模式”,订阅者和订阅目标是联系在一起的,当订阅目标发生改变时,逐个通知订阅者。
在现在的发布订阅模式中,已经独立于观察者模式,成为另外一种不同的设计模式。
在现在的发布订阅模式中,称为发布者的消息发送者不会将消息直接发送给订阅者,这意味着发布者和订阅者不知道彼此的存在。在发布者和订阅者之间存在第三个组件,称为调度中心或事件通道,它维持着发布者和订阅者之间的联系,过滤所有发布者传入的消息并相应地分发它们给订阅者。
可以看出,发布订阅模式相比观察者模式多了个事件通道,事件通道作为调度中心,管理事件的订阅和发布工作,彻底隔绝了订阅者和发布者的依赖关系。 即订阅者在订阅事件的时候,只关注事件本身,而不关心谁会发布这个事件;发布者在发布事件的时候,只关注事件本身,而不关心谁订阅了这个事件。
观察者模式有两个重要的角色,即目标和观察者。在目标和观察者之间是没有事件通道的。 一方面,观察者要想订阅目标事件,由于没有事件通道,因此必须将自己添加到目标(Subject) 中进行管理;另一方面,目标在触发事件的时候,也无法将通知操作(notify) 委托给事件通道,因此只能目标亲自去通知所有的观察者。
class EventEmitter {
constructor() {
this.callbacks = {}
}
on(event, fn) {
this.callbacks[event] = this.callbacks[event] || []
this.callbacks[event].push(fn)
}
emit(event) {
this.callbacks[event].forEach(fn => fn())
}
}
7. 观察者模式
意图:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
主要解决:一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。
何时使用:一个对象(目标对象)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。
如何解决:使用面向对象技术,可以将这种依赖关系弱化。
关键代码:在抽象类里有一个 ArrayList 存放观察者们。
观察者模式是一种对象行为型模式,其主要优点如下。
- 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。
- 符合依赖倒置原则。
- 目标与观察者之间建立了一套触发机制。
它的主要缺点如下。
- 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。
- 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率,并且可能会导致意外的更新。
- 观察者模式没有相应的机制让观察者知道所观察的目标对象是怎么发生变化的,而仅仅只是知道观察目标发生了变化。
class Observerd {
constructor(name ){
this.name = name;
this.state = "走路";
this.observers = [];
}
setObserver(observer) {
this.observers.push(observer)
}
setState(state) {
this.state = state;
this.observers.forEach(observer => observer.update(this))
}
}
class Observer {
constructor() {}
update(observerd) {
console.log(observerd.name + "正在" + observerd.state)
}
}
8. 寄生组合式继承
寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的形式来继承方法,只调用了一次父类构造函数,效率高,也避免了在子类的原型对象上创建不必要的、多余的属性,原型链也不会被改变,核心步骤有:
1. 在"Human"构造函数的原型上添加"getName"函数 2. 在”Chinese“构造函数中通过call函数借助”Human“的构造器来获得通用属性 3. Object.create函数返回一个对象,该对象的__proto__属性为对象参数的原型。此时将”Chinese“构造函数的原型和通过Object.create返回的实例对象联系起来 4. 最后修复"Chinese"构造函数的原型链,即自身的"constructor"属性需要指向自身 5. 在”Chinese“构造函数的原型上添加”getAge“函数
function Human(name) {
this.name = name
this.kingdom = 'animal'
this.color = ['yellow', 'white', 'brown', 'black']
}
function Chinese(name,age) {
Human.call(this, name);
this.age = age;
this.color = 'yellow'
}
function inherit(Human, Chinese) {
const prototype = Object.create(Human.prototype);
Chinese.prototype = prototype;
prototype.constructor = Chinese;
}
inherit(Human, Chinese);
Human.prototype.getName = function () {
return this.name;
}
Chinese.prototype.getAge = function () {
return this.age;
}
9. instanceof
instanceof 主要的实现原理就是 「只要右边变量的」 prototype 「在左边变量的原型链上即可」。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。
该函数可以判断首参是否在第二个Fn构造函数的原型链上,核心步骤有:1. 比较首个对象参数的原型对象和第二个Fn构造函数的原型对象 2. 判断首个对象参数的原型对象是否为空 3. 更改首个对象参数原型的原型为首参,递归该步骤直到null时返回false
const _instanceof = (target, Fn) => {
if (Fn.prototype === target.__proto__) {
return true;
}
if (target.__proto__ === null) return false;
return _instanceof(target.__proto__, Fn);
}
10. Array.map
该函数创建一个新数组,该新数组的结果是数组中的每个元素都调用一次函数参数后的返回值,核心步骤有:
1. 判断参数是否为函数,如果不是则直接返回 2. 创建一个空数组用于承载新的内容 3. 循环遍历数组中的每个值,分别调用函数参数,将返回值添加进空数组中 4. 返回新的数组
Array.prototype._map = function(Fn) {
if (typeof Fn !== "function") return;
// 数组
const array = this;
const result = [];
for(let i = 0; i < array.length; i++) {
result.push(Fn(array[i], i, array));
}
return result;
}
11. Array.filter
该函数创建一个新数组,该数组包含通过函数参数条件的所有元素,核心步骤有:
1. 判断参数是否为函数,如果不是则直接返回 2. 创建一个空数组用于承载新的内容 3. 循环遍历数组中的每个值,分别调用函数参数,将满足判断条件的元素添加进空数组中 4. 返回新的数组
Array.prototype._filter = function(Fn) {
if (typeof Fn !== "function") return;
const result = [];
const array = this;
for(let i = 0; i < array.length; i++) {
if (Fn(array[i], i, array)){
result.push(array[i]);
}
}
return result;
}
12. Array.reduce
根据Array.reduce的特点有:
- 接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值
- 可以接收一个初始值,当没有初始值时,默认初始值为数组中的第一项
实现该函数的核心步骤有: 1. 在Array的原型对象上添加”_reduce“函数 2. ”_reduce“函数第一个参数为回调函数,第二个参数为初始值 3. 进入数组长度的循环体中 4. 当初始值为空时,首个被加数为数组的第一项 5. 当初始值不为空时,首个被加数为初始值
Array.prototype._reduce = function(Fn, initValue) {
if (typeof Fn !== "function") return;
const array = this;
let result, i;
if (initValue) {
i = 0; result = initValue;
} else {
i = 1; result = array[0];
}
for(; i < array.length; i++) {
result = Fn(result, array[i]);
}
return result;
}
13. Function.call
该函数会临时修改内部this的指向并返回结果,核心步骤有:
1. 参数默认为window,否则为传入的首参 2. 获取调用该方法的对象,将this赋给对象属性,可以任意命名 3. 通过该对象属性临时调用函数并返回结果 4. 最后删除对象参数的临时函数属性
Function.prototype.myCall = function (obj, ...rest) {
var context = obj || window;
context.fn = this;
var result = context.fn(...rest);
delete context.fn;
return result;
}
14. Function.bind
一个函数被 call/apply 的时候,会直接调用,但是 bind 会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
Function.prototype._bind = function(target, ...arguments1) {
const context = target || window;
const that = this;
const bindArgs = arguments1;
return function () {
const args = Array.prototype.slice.call(arguments);
return that.apply(context, bindArgs.concat(args))
}
A bound function may also be constructed using the new operator: doing so acts as though the target function had instead been constructed. The provided this value is ignored, while prepended arguments are provided to the emulated function.
绑定函数也可以使用new操作符创建实例,这样做的行为就像是构造了目标函数一样,提供的this将被忽略
绑定函数继承目标函数
Function.prototype.myBind = function (context) {
// 获取绑定时的传参
let args = [...arguments].slice(1),
// 定义中转构造函数,用于通过原型连接绑定后的函数和调用bind的函数
F = function () {},
// 记录调用函数,生成闭包,用于返回函数被调用时执行
self = this,
// 定义返回(绑定)函数
bound = function () {
// 合并参数,绑定时和调用时分别传入的
let finalArgs = [...args, ...arguments]
// 改变作用域,注:aplly/call是立即执行函数,即绑定会直接调用
// 这里之所以要使用instanceof做判断,是要区分是不是new xxx()调用的bind方法
return self.call((this instanceof F ? this : context), ...finalArgs)
}
// 将调用函数的原型赋值到中转函数的原型上
F.prototype = self.prototype
// 通过原型的方式继承调用函数的原型
bound.prototype = new F()
return bound
}
15. Object.freeze
该函数可以冻结一个对象,一个被冻结的对象(第一层)不能被修改、不能添加新的属性、不能删除已有属性,核心步骤有:
1. 进入对象参数的遍历体中 2. 判断当前对象参数是否为普通对象或数组 3. 如果是普通对象或数组,则递归调用该函数,函数参数为当前遍历项 4. 如果不是,则直接设置该参数的可写性为false 5. 最后通过Object.seal函数封闭该对象,阻止添加新属性并将所有现有属性标记为不可配置
const _objectFreeze = object => {
for(let k in object) {
const type = Object.prototype.toString.call(object[k]);
if(type === '[Object object]' || type === '[Object array]') {
_objectFreeze(object[k]);
} else{
Object.defineProperty(object, k, {
writable: false,
})
}
}
Object.seal(object);
}
注意:
Object.seal 和 Object.freeze 区别
共同点 1. 执行后的对象变得不可扩展,这意味着对象将无法添加新属性。 2. 执行后的对象中的每个元素都变得不可配置,这意味着无法删除属性。 3. 如果在“使用严格”模式下调用操作,则两种方法都可能引发错误 不同 对象执行 Object.seal 后允许修改属性,而执行 Object.freeze 则不允许。 不足 Object.freeze 和 Object.seal 在“实用性”方面存在不足,它们都只是对对象的第一层有效, 浅冷冻。
16. Iterator
在JavaScript中,可以通过创建一个对象来实现Iterator接口,该对象至少包含next()方法。next()方法是一个无参数的函数,返回一个含有value和done属性的对象。value表示当前迭代的值,而done是一个布尔值,表示迭代是否结束。
let iterator = myIterator([1, 2]);
function myIterator(arr) {
let index = 0;
return {
next() {
return index < arr.length ?
{ value: arr[index++], done: false } :
{ value: undefined, done: true }
}
}
}
17. promise
// Promise/A+ 规范规定的三种状态
const STATUS = {
PENDING: "pending",
FULFILLED: "fulfilled",
REJECTED: "rejected",
};
class MyPromise {
// 构造函数接收一个执行回调
constructor(executor) {
this._status = STATUS.PENDING; // Promise初始状态
this._value = undefined; // then回调的值
this._resolveQueue = []; // resolve时触发的成功队列
this._rejectQueue = []; // reject时触发的失败队列
// 使用箭头函数固定this(resolve函数在executor中触发,不然找不到this)
const resolve = (value) => {
const run = () => {
// Promise/A+ 规范规定的Promise状态只能从pending触发,变成fulfilled
if (this._status === STATUS.PENDING) {
this._status = STATUS.FULFILLED; // 更改状态
this._value = value; // 储存当前值,用于then回调
// 执行resolve回调
while (this._resolveQueue.length) {
const callback = this._resolveQueue.shift();
callback(value);
}
}
};
//把resolve执行回调的操作封装成一个函数,放进setTimeout里,以实现promise异步调用的特性(规范上是微任务,这里是宏任务)
setTimeout(run);
};
// 同 resolve
const reject = (value) => {
const run = () => {
if (this._status === STATUS.PENDING) {
this._status = STATUS.REJECTED;
this._value = value;
while (this._rejectQueue.length) {
const callback = this._rejectQueue.shift();
callback(value);
}
}
};
setTimeout(run);
};
// new Promise()时立即执行executor,并传入resolve和reject
executor(resolve, reject);
}
// then方法,接收一个成功的回调和一个失败的回调
then(onFulfilled, onRejected) {
// 根据规范,如果then的参数不是function,则忽略它, 让值继续往下传递,链式调用继续往下执行
typeof onFulfilled !== "function" ? (onFulfilled = (value) => value) : null;
typeof onRejected !== "function" ? (onRejected = (error) => error) : null;
// then 返回一个新的promise
return new MyPromise((resolve, reject) => {
const resolveFn = (value) => {
try {
const x = onFulfilled(value);
// 分类讨论返回值,如果是Promise,那么等待Promise状态变更,否则直接resolve
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
} catch (error) {
reject(error);
}
};
const rejectFn = (error) => {
try {
const x = onRejected(error);
x instanceof MyPromise ? x.then(resolve, reject) : resolve(x);
} catch (error) {
reject(error);
}
};
switch (this._status) {
case STATUS.PENDING:
this._resolveQueue.push(resolveFn);
this._rejectQueue.push(rejectFn);
break;
case STATUS.FULFILLED:
resolveFn(this._value);
break;
case STATUS.REJECTED:
rejectFn(this._value);
break;
}
});
}
catch(rejectFn) {
return this.then(undefined, rejectFn);
}
// promise.finally方法
finally(callback) {
return this.then(
(value) => MyPromise.resolve(callback()).then(() => value),
(error) => {
MyPromise.resolve(callback()).then(() => error);
}
);
}
// 静态resolve方法
static resolve(value) {
return value instanceof MyPromise
? value
: new MyPromise((resolve) => resolve(value));
}
// 静态reject方法
static reject(error) {
return new MyPromise((resolve, reject) => reject(error));
}
// 静态all方法
static all(promiseArr) {
let count = 0;
let result = [];
return new MyPromise((resolve, reject) => {
if (!promiseArr.length) {
return resolve(result);
}
promiseArr.forEach((p, i) => {
MyPromise.resolve(p).then(
(value) => {
count++;
result[i] = value;
if (count === promiseArr.length) {
resolve(result);
}
},
(error) => {
reject(error);
}
);
});
});
}
// 静态race方法
static race(promiseArr) {
return new MyPromise((resolve, reject) => {
promiseArr.forEach((p) => {
MyPromise.resolve(p).then(
(value) => {
resolve(value);
},
(error) => {
reject(error);
}
);
});
});
}
}