本文内容来源于网络、网课以及作者自己的归纳总结,js系列文章共计四篇,知识点比较多也比较杂,文章仅作简单的总结,不作过多深入分析。系列文章将持续更新。如有不妥之处,请在评论区友好指出~
一、设计模式
1.1 分类
常见的设计模式有23种,大致可分为三种类型
- 创建型模式:主要解决对象创建问题
- 结构型模式:主要解决对象组合问题
- 行为型模式:主要解决对象之间的交互问题
下文仅简单分析JavaScript中常见的几种设计模式
1.2 创建型模式
1、单例模式
目的:需要确保全局只有一个对象(为了避免重复新建,避免多个对象存在互相干扰)
设计方式:通过定义一个方法,使用时只允许通过此方法拿到存在内部的同一实例化对象。在js中通过全局对象或闭包实现单例模式很简单
let Singleton = function (name) {
this.name = name;
};
Singleton.genInstance = function (name) {
if (this.instance) return this.instance;
return (this.instance = new Singleton(name));
};
const instanceOne = Singleton.genInstance("lihua");
const instanceTWO = Singleton.genInstance("xiaoming"); // 不会再新建
console.log(instanceOne, instanceTWO); // instanceOne === instanceTWO
案例1:状态管理,即使多次创建实例,也指向同一个store
function store() {
this.store = {};
if (store.install) {
return store.install;
}
store.install = this;
}
var s1 = new store();
var s2 = new store();
s1.store.name = "lin";
console.log(s2);
//输出:{ store:{ name:'lin' } }
// 这样以来,只要创建实例,都会指向同一个对象
案例2:vue-router保障全局有且只有一个,否则会错乱
let _Vue
function install(Vue) {
if(install.installed && _Vue === vue) return
install.installed = true
_Vue = Vue
}
vue.use(router) //每次执行都会指向同一个对象
2、工厂模式
目的:当某一个对象需要经常创建的时候。js中的函数可以充当工厂函数,根据传入参数的同创建不同类型的对象
设计方式:写一个方法,只需要调用该方法,就能拿到想要的对象
function fn(type) {
switch (type) {
case:'type1':
return new Type1() //根据类型创建对象
//...
}
}
案例1:创建多个不同的弹窗对象
function infoPop(content, color) {
this.content = content;
this.color = color;
}
function confirmPop(content, color) {
// ...
}
function pop(type, content, color) {
switch (type) {
case "infoPop":
return new infoPop(content, color);
case "confirmPop":
return new confirmPop(content, color);
}
}
// 创建对象
const popInstance1 = pop("infoPop", "hello", "red");
const popInstance2 = pop("confirmPop", "world", "yellow");
1.3 结构型模式
1、适配器模式
目的:处理不兼容的接口或方法之间的适配问题(强调将一个接口转换为另外一个接口)
设计方式:新增一个包装类,对新的方法/接口进行包装以适应旧代码的调用
// 定义目标接口
function TargetInterface() {
this.request = function () {
// ...
};
}
// 定义适配者
function Adaptee() {
this.specificRequest = function () {
// ...
};
}
// 创建适配器
function Adapter() {
const adaptee = new Adaptee();
this.request = function () {
adaptee.specificRequest();
// 执行适配逻辑
};
}
// 使用适配器
const adapter = new Adapter();
adapter.request();
案例一:例如你有一个自研框架,其中有方法在你依赖的库中已经实现,那么完全可以不用再写一次该方法(以lodash为例)
myMethods.deepClone = function() {
return _.cloneDeep.call(this, arguments)
}
2、装饰器模式
目的:在不修改原始对象的情况下,为现有的对象拓展额外的功能
设计方式:假设为一个函数添加新功能,创建一个新的函数并以原始函数为参数,最后返回一个新的函数
案例一:为耗时函数添加缓存功能
// 原始函数
function calculate(num) {
let result = 0;
for (let i = 0; i < num; i++) {
result += i;
}
return result;
}
// 装饰器函数
function cache(fn) {
const cache = new Map(); //闭包
return function (num) {
if (cache.has(num)) return cache.get(num);
else {
const result = fn(num);
cache.set(num, result);
return result;
}
};
}
const cachedCalculate = cache(calculate);
console.log(cachedCalculate(99999)); // 执行耗时计算
console.log(cachedCalculate(99999)); // 从缓存中获取结果
案例二:当需要去改动他人的代码,往其中增加新功能时。例如给某些点击事件新增操作提示
function decorator(dom, fn) {
if (typeof dom.onclick === "function") {
let _old = dom.onclick;
//重写
dom.onclick = function () {
//调用老方法
_old();
//执行新的操作
fn();
};
}
}
3、代理模式
目的:类似于es6的proxy,对目标对象的访问能够被拦截下来,并执行一些其他的操作
// 代理的对象
function TargetFN() {
this.request = function () {
// ...
};
}
// 定义代理处理函数
function Proxy() {
const targetFN = new TargetFN();
this.request = function () {
// 执行一些前置操作
targetFN.request();
// 执行一些后置操作
};
}
const p = new Proxy();
p.request();
1.4 行为型模式
1、策略模式
目的:定义一些列算法,将其逐个封装起来,目的在于分离算法的使用与实现,优化代码,减少重复代码
案例一:优化多重if判断语句
let type = 1;
if (type === 1) {
console.log("people");
} else if (type === 2) {
console.log("bird");
} else if (type === 3) {
console.log("fish");
} else if (type === 4) {
//...
}
// 使用策略模式
const actions = {
1: () => { console.log("people") },
2:() => { console.log("bird") },
3: () => { console.log("fish") },
// ...
};
actions[type]()
案例二:优化多重if判断语句嵌套
let type = 1; //物种类型
let actionsType = 1; //吃饭1,睡觉2,游泳3....
if (type === 1) {
if (actionsType === 1) {
// do something
} else if (actionsType === 2) {
// do something
} else if (actionsType === 3) {
// do something
}
} else if (type === 2) {
if (actionsType === 1) {
// do something
} else if (actionsType === 2) {
// do something
} else if (actionsType === 3) {
// do something
}
} else if (type === 3) {
// ...
} else if (type === 4) {
//...
}
// 使用策略模式(该部分最终可以抽离出去)
const actions = [
{
type: 1,
actionsType: 1,
fn: () => {
//do something
},
},
{
type: 1,
actionsType: 2,
fn: () => {
//do something
},
},
{
type: 1,
actionsType: 3,
fn: () => {
//do something
},
},
{
type: 2,
actionsType: 1,
fn: () => {
//do something
},
},
// ...
];
let fn = actions.find(item => item.type === type && item.actionsType === actionsType);
fn();
2、 观察者模式
在一些文章中,把观察者称为发布订阅模式。但是二者其实有一些小区别
观察者模式指的是一个对象(Subject)维持一系列依赖于它的对象(Observer),当有关状态发生变更时 Subject 对象则通知一系列 Observer 对象进行更新
1、结构:两个主要角色:观察者(Observer)和被观察者(Subject)
2、联系:观察者直接订阅被观察者,被观察者维护一个观察者列表,并在状态变化时通知所有观察者(通常调用观察者的方法进行通知)
案例一
以某个手机的销售场景为例。假设现有一爆款手机在手机店卖断了货,手机店(Subject被观察者)告诉前来的客户(Observer观察者)说把姓名联系方式留下(触发add操作),后续有货了通知(触发notify)客户来买
// Subject 对象(客户在这里留了姓名联系方式)
function Subject() {
this.observers = [];
}
Subject.prototype = {
add(observer) {
// 添加(添加客户的姓名联系方式)
this.observers.push(observer);
},
notify() {
// 通知(通知客户有货了)
var observers = this.observers;
for (var i = 0; i < observers.length; i++) {
observers[i].update();
}
},
};
// Observer 对象
function Observer(name) {
this.name = name; // (用户信息、联系方式等)
}
Observer.prototype = {
update() {
// 更新(通知客户来取货)
console.log("my name is " + this.name);
},
};
var sub = new Subject();
var obs1 = new Observer("客户1");
var obs2 = new Observer("客户2");
sub.add(obs1);
sub.add(obs2);
sub.notify();
1.5 发布订阅模式
发布订阅模式:订阅者(Subscriber)把自己想订阅的事件注册到(subscribe)调度中心(中介者)。当发布者(Publisher)发布该事件到调度中心(也就是事件触发时),由调度中心统一调用订阅者注册的处理代码
1、结构:存在一个中介者来订阅和发布者之间的关系
2、联系:发布者和订阅者不直接耦合,两者通过中介者进行通信。发布者将消息发送到中介者,然后中介者将消息传递给所有订阅者
3、通知方式: 订阅者通过向中介者注册感兴趣的事件或主题,中介者在接收到消息后负责将消息分发给所有订阅者
简单案例
class EventEmitter {
constructor() {
this.event = [];
}
subscribe(callback) {
this.event.push(callback);
}
publish(value) {
this.event.forEach((callback) => {
callback(value);
});
}
}
const ev = new EventEmitter();
// 订阅
ev.subscribe((args) => {
console.log(args + ",callback111");
});
ev.subscribe((args) => {
console.log(args + ",callback222");
});
// 发布
ev.publish("hello");
案例一:事件总线
class EventBus {
constructor() {
this.events = {}; // 存储事件及其对应的回调函数列表(中介者或者说调度中心)
}
// 订阅事件
subscribe(eventName, callback) {
this.events[eventName] = this.events[eventName] || []; // 如果事件不存在,创建一个空的回调函数列表
this.events[eventName].push(callback);
}
// 发布事件
publish(eventName, data) {
if (this.events[eventName]) {
this.events[eventName].forEach((callback) => {
callback(data); // 执行回调函数,并传递数据作为参数
});
}
}
// 取消订阅事件
unsubscribe(eventName, callback) {
if (this.events[eventName]) {
this.events[eventName] = this.events[eventName].filter(
(cb) => cb !== callback
); // 过滤掉要取消的回调函数
}
}
}
// 创建全局事件总线对象
const eventBus = new EventBus();
const callback1 = (data) => {
console.log("Callback 1:", data);
};
const callback2 = (data) => {
console.log("Callback 2:", data);
};
// 订阅事件
eventBus.subscribe("event1", callback1);
eventBus.subscribe("event1", callback2);
// 发布事件
eventBus.publish("event1", "Hello, world!");
// 取消订阅事件
eventBus.unsubscribe("event1", callback1);
// 发布事件
eventBus.publish("event1", "ahahhaha!");
案例二:利用发布订阅实现防止接口重复调用
import axios from "axios";
let instance = axios.create({
baseURL: "/api",
});
// 发布订阅
class EventEmitter {
constructor() {
this.event = {};
}
on(type, cbres, cbrej) {
if (!this.event[type]) {
this.event[type] = [[cbres, cbrej]];
} else {
this.event[type].push([cbres, cbrej]);
}
}
emit(type, res, ansType) {
if (!this.event[type]) return;
else {
this.event[type].forEach((cbArr) => {
if (ansType === "resolve") {
cbArr[0](res);
} else {
cbArr[1](res);
}
});
}
}
}
// 根据请求生成对应的key
function generateReqKey(config, hash) {
const { method, url, params, data } = config;
return [method, url, JSON.stringify(params), JSON.stringify(data), hash].join(
"&"
);
}
// 存储已发送但未响应的请求
const pendingRequest = new Set();
// 发布订阅容器
const ev = new EventEmitter();
// 添加请求拦截器
instance.interceptors.request.use(
async (config) => {
let hash = location.hash;
// 生成请求Key
let reqKey = generateReqKey(config, hash);
if (pendingRequest.has(reqKey)) {
// 如果是相同请求,在这里将请求挂起,通过发布订阅来为该请求返回结果
// 这里需注意,拿到结果后,无论成功与否,都需要return Promise.reject()来中断这次请求,否则请求会正常发送至服务器
let res = null;
try {
// 接口成功响应
res = await new Promise((resolve, reject) => {
ev.on(reqKey, resolve, reject);
});
return Promise.reject({
type: "limiteResSuccess",
val: res,
});
} catch (limitFunErr) {
// 接口报错
return Promise.reject({
type: "limiteResError",
val: limitFunErr,
});
}
} else {
// 将请求的key保存在config
config.pendKey = reqKey;
pendingRequest.add(reqKey);
}
return config;
},
function (error) {
return Promise.reject(error);
}
);
// 添加响应拦截器
instance.interceptors.response.use(
function (response) {
// 将拿到的结果发布给其他相同的接口
handleSuccessResponse_limit(response);
return response;
},
function (error) {
return handleErrorResponse_limit(error);
}
);
// 接口响应成功
function handleSuccessResponse_limit(response) {
const reqKey = response.config.pendKey;
if (pendingRequest.has(reqKey)) {
let x = null;
try {
x = JSON.parse(JSON.stringify(response));
} catch (e) {
x = response;
}
pendingRequest.delete(reqKey);
ev.emit(reqKey, x, "resolve");
delete ev.reqKey;
}
}
// 接口走失败响应
function handleErrorResponse_limit(error) {
if (error.type && error.type === "limiteResSuccess") {
return Promise.resolve(error.val);
} else if (error.type && error.type === "limiteResError") {
return Promise.reject(error.val);
} else {
const reqKey = error.config.pendKey;
if (pendingRequest.has(reqKey)) {
let x = null;
try {
x = JSON.parse(JSON.stringify(error));
} catch (e) {
x = error;
}
pendingRequest.delete(reqKey);
ev.emit(reqKey, x, "reject");
delete ev.reqKey;
}
}
return Promise.reject(error);
}
export default instance;
发布订阅模式与观察者模式的区别?
1、通信方式的差异:观察者模式中,被观察者直接通知观察者;而在发布订阅模式中,发布者通过中介者联系订阅者
2、直接耦合与间接耦合:观察者模式中,观察者与被观察者直接耦合;发布订阅模式中,发布者和订阅者通过中介者间接耦合
二、面向对象
JavaScript其实支持多种编程范式的,包括函数式编程和面向对象编程
面向对象:
1、JavaScript中的对象被设计成一组属性的无序集合,像是一个哈希表,由key和value组成
2、key是一个标识符名称,value可以是任意类型,也可以是其他对象或者函数类型
3、如果值是一个函数,那么我们可以称之为是对象的方法
创建对象
//方式一: new关键字
var obj = new Object()
//方式二: 字面量
var obj = {}
操作对象
//赋值
obj.name = "linming"
//取值
obj.name
//删除值
delete obj.name
有时候我们也希望对一个属性进行比较精准的操作控制,比如不允许对象的某个值被删除、被赋值、不允许哪一个值被遍历等等,这个时候,就可以使用Object.defineProperty
2.1 defineProperty
Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象
可接收三个参数
1、obj:要定义属性的对象
2、prop:要定义或修改的属性的名称或Symbol
3、descriptor:要定义或修改的属性描述符
返回值:被修改的对象obj
var obj = {
name: "xiaoming",
age: 19
}
// 为obj增加一个属性
Object.defineProperty(obj, "height", {
value: 1.88
})
console.log(obj) //输出:{ name: 'linmnig', age: 19 }
注:通过该函数添加的新属性,默认不可被遍历
属性描述符分类
属性描述符的类型有两种:
1、数据属性描述符
2、存取属性(访问器)描述符
| 分类 | configurabel | enumerable | value | writable | get | set |
|---|---|---|---|---|---|---|
| 数据描述符 | 可以 | 可以 | 可以 | 可以 | 不可以 | 不可以 |
| 存取描述符 | 可以 | 可以 | 不可以 | 不可以 | 可以 | 可以 |
如何区分:当存在get、set时,不能有value和writable(不能共存),反之同理
数据描述符的特性
1、
[[Configurable]]:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符A、直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true
B、通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false
2、[[Enumerable]]:表示是否可以通过遍历获得该属性
A、直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true
B、通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false
3、[[Writable]]:表示是否可以修改属性的值
A、直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true
B、通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false
4、[[value]]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改
- A、默认情况下这个值是undefined
var obj = {
name: 'xiaoming'
}
//原先obj并没有age这个属性
Object.defineProperty(obj, 'age', {
//默认值为undefined
//age默认不可修改或删除
//age不可被遍历
})
存取属性描述符
1、[[Configurable]]——与数据属性描述符一致
2、[[Enumerable]]——与数据属性描述符一致
3、[[get]]:获取属性时会执行的函数,默认为undefined
4、[[set]]:设置属性时会执行的函数,默认为undefined
let obj = {
name: 'xiaoming',
_address: '广州'
}
Object.defineProperty(obj, "address", {
enumerable: true, //可遍历
configurable: true, //可删除
get: function() {
return this._address
},
set: function(value) {
this._address = value
}
})
get、set的应用
1、隐藏一个私有属性,不希望直接被外界使用和赋值
2、希望截获某一个属性访问时,设置值时的过程
Object.defineProperty(obj, "address", {
enumerable: true, //可遍历
configurable: true, //可删除
get: function() {
foo()
return this._address
},
set: function(value) {
bar()
this._address = value
}
})
function foo() {
console.log("取值前截获到了");
}
function bar() {
console.log("设置值前截获到了");
}
console.log(obj.address);
console.log(obj.address = "深圳");
Object.defineProperties()
Object.defineProperties()方法直接在一个对象上定义多个新的属性或修改现有属性,并且返回该对象
例如:同时配置name、age属性
const obj = {
_age: 18
}
Object.defineProperties(obj, {
name: {
configurable: true,
enumerable: true,
writableL: true,
value: "xiaoming"
},
age: {
configurable: false,
enumerable: false,
get: function() {
return this._age
},
set: function(value) {
this._age = value
}
}
})
获取某一个属性的属性描述符
getOwnPropertyDescriptor()
console.log(Object.getOwnPropertyDescriptor(obj, "age"));
//输出:
//{
// get: [Function: get],
// set: [Function: set],
// enumerable: false,
// configurable: false
//}
获取对象的所有属性描述符
Object.getOwnPropertyDescriptors()
console.log(Object.getOwnPropertyDescriptors(obj));
2.2 批量创建对象
1、工厂模式
工厂模式是一种常见的设计模式
通常我们会有一个工厂方法,通过该工厂方法我们可以产生想要的对象
//基本结构
function createPerson () {
var p = {}
return p
}
//创建对象实例
var p1 = createPerson()
具体案例如下
function createPerson (name, age, height, address) {
var p = {}
p.name = name
p.age = age
p.height = height
p.address = address
p.eating = function() {
this.name + "在吃东西"
}
p.running = function() {
this.name + "在跑步"
}
return p
}
var p1 = createPerson('xiaoming', 22, 1.81, "广州")
console.log(p1.eating);
console.log(p1.name);
工厂模式的缺点:获取不到对象的具体类型,我们在打印对象时,对象的类型都是Obejct类型
2、构造函数
构造函数也称之为构造器(constructor),通常是我们在创建对象时会调用的函数
如果一个普通的函数被使用new操作符来调用了,那么这个函数就称之为是一个构造函数
function foo() {
console.log("foo~")
}
//普通方式调用
foo() //输出:foo~
//new调用
new foo() //输出:foo~
问题:new调用有什么特殊的地方吗?
如果一个函数被使用new操作符调用了,那么它会执行如下操作:
1、在内存中创建一个新的对象(空对象)
2、这个对象内部的[[prototype]]属性(隐式原型)会被赋值为该构造函数的prototype属性
3、构造函数内部的this,会指向创建出来的新对象
{} = this4、执行函数的内部代码(函数体代码)
5、如果构造函数没有返回非空对象,则返回创建出来的新对象
function Person() {}
var p1 = new Person() //创建一个{},this = {} ,执行函数体代码,返回新对象
console.log(p1) //输出:Person {}
完整的案例代码
function Person (name, age, height, address) {
//1.new 调用时创建一个{}
//2.这里的this = 那个空对象
//执行函数体,往对象里边添加属性、方法
this.name = name
this.age = age
this.height = height
this.address = address
this.eating = function() {
console.log(this.name + "在吃东西~")
}
}
var p1 = new Person("lihua", "18", "2.00", "深圳市")
var p2 = new Person('xiaoming', 22, "1.92", "广州市")
console.log(p1);
p1.eating()
p2.eating()
console.log(p1.eating === p2.eating) //false
构造函数的缺点:从上边的案例中,console.log(p1.eating === p2.eating) //false 我们可以得出,创建出来的实例的函数地址是不同的(即内部创建了不同的内存空间来保存这个函数),但是完全没有必要。但这样子的创建实例多了,就浪费了很多的内存空间
2.3 对象的原型
隐式原型
JavaScript当中每个对象都有一个特殊的内置属性[[prototype]],这个特殊的属性可以指向另一个对象
let obj = { name: 'linming' } //[[prototype]]
如何查看这个属性?
1、方式一:__proto__
通过对象的这个属性可以获取到
let obj = {}
console.log(obj.__proto__)
//输出:[Object: null prototype] {}
2、方式二:Object.getPrototypeOf()
let obj = {}
console.log(Object.getPrototypeOf(obj))
//输出:[Object: null prototype] {}
从上边的代码中,都获取到了一个空对象,这个对象有什么用呢?
当我们通过引用对象的属性key来获取一个value时,它会触发[[Get]]的操作,这个操作会首先检查该属性是否对应的属性,如果有的话就使用它;如果没有该属性,那么会访问对象的[[prototype]]内置属性指向的对象上的属性
let obj = {}
obj.__proto__.name = "linming"
console.log(obj.name) //linming
2.4 函数的原型
1、函数本质上也是一个对象,所以它有隐式原型
function foo() {}
console.log(foo.__proto__) // {}
2、函数还有一个显示原型属性:prototype
console.log(foo.prototype) // {}
思考:函数的prototype也是一个对象,那么这个对象的隐式原型是谁呢?
console.log(foo.prototype.__proto__) //输出:[Object: null prototype] {},即Object的原型对象
3、当函数作为构造函数被调用时,其内部创造出来的对象的隐式原型会指向显示原型
我们之前讨论过通过new调用一个函数会发生什么?
其中的第二步:这个对象内部的[[prototype]]属性(即隐式原型)会被赋值为该构造函数的prototype属性
function Foo() {
//1.创建出 {}
//2. {}.__proto__ = Foo.prototype
//3.this = {}
//4.执行代码
//5.return this
}
所以,构造函数创建出来的实例,它的隐式原型 === 它的显示原型
let f1 = new Foo()
let f2 = new Foo()
console.log(f1.__proto__ === Foo.prototype)
console.log(f2.__proto__ === Foo.prototype)
所以,这样一来,往Foo.prototype中添加属性,在f1,f2都能访问
Foo.protype.name = 'linming'
console.log(f1.name) //linming
console.log(f2.name) //linming
//通过f1,或者f2向原型对象添加属性
f1.__proto__.age = 22
f2.__proto__.age = 18
console.log(f1.age) //输出:18
4、constructor属性
function Foo() {}
console.log(Foo.prototype) //输出:{}
从上边可以知道,Foo的显示原型对象是空的。
实际上,它不是空的,只是这个原型对象的enumerable属性被设置为false,所以我们不能遍历出其中的属性
原型对象上有一个constructor属性,指向Foo函数本身
console.log(Foo.prototype.constructor)
//输出:[Function: Foo]
所以,下面这样写也是允许的
//获取函数名
console.log(Foo.prototype.constructor.name) //输出:Foo
//也可以这样
console.log(Foo.prototype.constructor.prototype.constructor.prototype.constructor)
//输出:[Function: Foo]
5、修改原型对象
有时候我们不想用原来的原型对象,可以这么做
function Foo() {}
Foo.prototype = {} //直接修改了整个prototype
这样的结果是,我们直接断开了指向原本的原型对象,转而指向这个新的{}
Foo.prototype = {
name: 'linming',
age: 22,
height:1.78
}
当然这个新创建的{}对象并不完美,因为 其中没有constructor属性
真实开发中,我们可以通过Object.defineProperty方法添加constructor
Object.defineProperty(Foo.prototype, "constructor", {
enumerable: false, //原来的原型对象的constructor是不能被遍历的
writable: true,
configurable: true,
value: Foo //指向本身
})
测试代码
let f1 = new Foo()
let f2 = new Foo()
console.log(f1.name, f2.age, f2.height);
console.log(Foo.prototype.constructor.name); //Foo
2.5 原型链
我们知道,查找一个对象的属性和方法时,会触发[[ get ]]操作
1、在当前对象中查找
2、如果没有找到,将会去对象的原型上(__proto__)查找
const obj = {}
obj.__proto__.address = "广州市"
console.log(obj.address) //输出:广州市
3、如果还没找到,会继续沿着原型链查找(原型对象是一个对象,对象有自己的原型对象)
obj.__proto__ = {}
obj.__proto__.__proto__ = {}
obj.__proto__.__proto__.__proto__ = {
address: 'linming'
}
console.log(obj.address) //输出:linming
但是原型链不是无限的,最终会止于最顶层原型
那么最顶层的原型是是谁呢?就是Object的原型对象
2.6 Object的原型
Object本质上是一个构造函数,Object也是js所有类的父类
const obj = {}
console.log(obj.__proto__)
//输出:[Object: null prototype] {}
这里输出的obj的隐式原型就是Object的显式原型对象[Object: null prototype] {}
为什么呢?为什么obj的隐式原型可以打印出来的就是顶层原型?
重点理解
const obj = {}
//等于
const obj = new Obejct()
事实上字面量定义一个对象,就是new Object()的语法糖
结合前面说过的调用new创建出一个对象所经过的步骤,就不难理解
const obj = new Object()
//1.创建出一个{}
//2.this = {}
//3.{}.__proto__ = Object.prototype
//4.执行函数体
//5.返回这个对象
所以,这样一来,obj.__proto__ === Object.prototype
那么Object的原型对象里边都有什么呢?
有很多默认的属性和方法
console.log(Object.getOwnPropertyDescriptors(Object.prototype))
//输出:
//constructor
//defineGetter__
//defineSetter__
// __lookupGetter__
//__lookupSetter__
//isPrototypeOf
//toString
//valueOf
//__proto__
//...
注意:其中,顶层显式原型对象是一个对象,它的隐式原型是null
const obj = new Object()
console.log(obj.__proto__.__proto__) //输出:null
构造函数的顶层原型对象
思考:构造函数的显式原型也是一个对象,这个对象是否有[[prototype]]隐式原型属性呢?
答案是肯定的
function People() {}
console.log(People.prototype.__proto__)
//[Object: null prototype] {}
并且该People构造函数的原型对象的原型指向最顶层的Object的原型
所以,可以说People构造函数继承自Object
2.7 对象-函数-原型的联系
1、对象存在隐式原型
const obj = {}
//相当于
const obj = new Object() //new操作,使obj的隐式原型指向Object的显式原型
console.log(obj.__proto__) //顶层Object的显示原型:[Object: null prototype] {}
console.log(obj.__proto__ === Object.prototype) //true
2、函数存在隐式原型、也存在显式原型
隐式原型(对象):Foo.__proto__
显式原型(对象):Foo.prototype
//为什么FOO函数也是一个对象?
let Foo = new Function()
Funtion是Foo的父类(同时,Function本身也是一个对象、也是一个构造函数)
函数的显式原型与隐式原型是不相等的
console.log(Foo.__proto__ === Foo.prototype) //false
//Foo.protorype来自哪里
//答案:创建一个函数时,js引擎自动为其添加的
Foo.prototype = { constructor: Foo }
//Foo.__proto__来自哪里
//答案:new Function(),即Function对象的显式原型
// Foo.__proto__ === Function.prototype
3、关系图
特别注意:
A、Function.__proto__ === Function.prototype(自己创造了自己)
B、Object函数对象也可以理解成const Object = new Function(),所以它的隐式原型指向Function的显式原型
创建一些实例,其指向关系如何
总结
1、对象
在js中,每个对象都存在一个隐式原型,默认是一个空对象,可以通过.__proto__访问
当我们访问js对象中的属性时,如果对象自身没有,则会沿着隐式原型查找,找到则返回,直到顶层对象的显示原型
2、函数
函数因为本质也是对象,所以也存在隐式原型
同时函数也存在显示原型,通过prototype访问,显示原型里有一个constructor属性指向函数本身,同时因为显示原型也是一个对象,它也包含了隐式原型,这个隐式原型指向了顶层对象的原型
3、顶层Object
顶层Object是一个构造函数,它的显示原型中存在很多的方法,例如toString等
它的隐式原型是null
4、构造函数
当我们创建一个实例对象时,默认这个实例对象的隐式原型会指向构造函数的显示原型。所以我们可以通过原型链一层层地查找想要的属性,直到顶层Obecjt
2.8 原型内容补充
1、hasOwnProperty
判断对象是否有一个属于自己的属性(不是在原型上的属性)
var obj = {
name: "linming",
age: 18,
};
obj.__proto__.address = "深圳";
console.log(obj.hasOwnProperty("address")); //false
console.log(obj.hasOwnProperty("name")); //true
2、in操作符
判断某个属性是否在某个对象或者对象的原型上
console.log("address" in obj) //true
console.log("name" in obj) //true
3、for...in
该遍历操作可以遍历出原型上的属性
for (let key in obj) {
console.log(key)
}
// address name age
4、instanceof
用于检测构造函数的prototype,是否出现在某个实例对象的原型链上
假设Student构造函数继承自People类
var stu = new Student()
console.log(stu instanceof Student) //true
console.log(stu instanceof People) //true
console.log(stu instanceof Object) //true
2.9 继承
1、原型链继承
// 父类
function People() {
this.name = "linming";
this.obj = { age: 20 }
}
People.prototype.running = function () {
console.log(this.name + "在跑步");
};
// 子类
function Child() {
this.age = 22;
}
// 继承:改写了Child的显示原型,指向People的实例
Child.prototype = new People();
Child.prototype.eating = function () {
console.log(this.name + "在吃东西");
};
const son = new Child();
console.log(son.age); //22
console.log(son.name); //linming
son.running(); //linming在跑步
son.eating(); //linming在吃东西
最主要的代码是Child.prototype = new People(),这里将Child的原型改写为People的实例,由此实现了继承
缺点:父类的引用属性会被所有子类共享,一处修改会影响所有子类;子类实例不能传递参数给父类构造函数
2、构造函数继承
function People(name, age, address) {
this.name = name;
this.age = age;
this.address = address;
}
function Child(name, age, address, sno) {
// this = 所创建的实例
People.call(this, name, age, address); //相当于在这里执行了People所有初始化方法
this.sno = sno;
}
const son1 = new Child("linming", 22, "深圳", 20180201);
const son2 = new Child("小明", 223, "广州", 201803041);
console.log(son1);
console.log(son2);
缺点:子类访问不了父类原型上定义的方法
3、组合式继承
组合式继承结合了原型链继承和构造函数继承的特点
function People(name, age, address) {
this.name = name;
this.age = age;
this.address = address;
}
People.prototype.running = function () {
console.log(this.name + " is running");
};
function Child(name, age, address, sno) {
// this = 所创建的实例
People.call(this, name, age, address); //相当于在这里执行了People所有初始化方法
this.sno = sno;
}
Child.prototype = new People(); //Child的隐式原型即可指向People的显示原型
const son1 = new Child("xiaoming", 22, "深圳", 20180201);
console.log(son1);
son1.running();
优点:父类方法可以复用;允许子类向父类传参;父类中的引用属性不会被共享
4、原型式继承
let person = {
name: "xiaoming",
age: 22,
friends: ["lihua", "tom", "make"],
sayName: function () {
console.log(this.name);
},
};
function objectCopy(obj) {
function Fun() {}
Fun.prototype = obj; //让子类的原型指向person
return new Fun();
}
let p1 = objectCopy(person);
let p2 = objectCopy(person);
p1.friends.push("xxx");
console.log(p2.friends); //多了xxx
缺点:父类的引用会被所有子类共享;子类实例不能向父类传参
5、寄生式继承
let person = {
name: "xiaoming",
friends: ["lihua", "make", "jack"],
};
// 创建一个对象,并且允许自定义隐式原型的指向
function copyObject(obj) {
function Fun() {}
Fun.prototype = obj;
return new Fun();
}
function createChild(original) {
let clone = copyObject(original);
clone.running = function () {
console.log(this.name + " is running");
};
return clone;
}
const son1 = createChild(person);
const son2 = createChild(person);
son1.friends.push("被共享");
console.log(son2.friends); //['lihua', 'make', 'jack', '被共享']
6、寄生组合式继承
// 父类
function People(name) {
this.name = name;
this.friends = ["lihua", "jack", "tom"];
}
// 子类
function Child(name, age) {
People.call(this, name); //继承父类的属性方法
this.age = age; // 子类自己的属性方法
}
// 创建一个对象,并且允许自定义隐式原型的指向
function copyObject(obj) {
function Fun() {}
Fun.prototype = obj;
return new Fun();
}
function inheritPrototype(child, parent) {
let prototype = copyObject(parent.prototype); // 创建对象,对象的隐式原型 = 父类原型
prototype.constructor = child;
child.prototype = prototype;
}
People.prototype.sayName = function () {
console.log(this.name);
};
inheritPrototype(Child, People); //改造子类的原型
Child.prototype.sayAge = function () {
console.log(this.age);
};
let son1 = new Child("xiaoming", 22);
let son2 = new Child("lily", 20);
son1.friends.push("不会被共享");
console.log(son2.friends); // ["lihua", "jack", "tom"];
son2.sayAge(); // 20
son2.sayName(); // lily
经过这么一番改造后,子类的显示原型被重新赋值了一个对象,并且该对象的隐式原型指向父类的显示原型。因此子类可以访问自己的原型和父类的原型(两者是分隔开的)
另一方面,子类自身拥有父类自身所有的属性和方法(可自定义传参),也可以拥有自己的属性和方法。因此多个子类实例不用担心引用属性被共享的问题
三、class类
3.1 class基础
1、定义类
类本质上就是构造函数的一种语法糖
//普通写法
class People { }
//字面量写法
const People = class { }
2、分析类
因为类是函数,也有隐式原型、显示原型
//隐式原型
console.log(People.__proto__) //输出: {}
//显示原型
console.log(People.prototype) //输出: {}
console.log(People.prototype.constructor) //输出:[class People]
如果检测一个class属于哪个数据类型,最终也会返回function
console.log(typeof People); //输出:function
3、类的构造函数
每个类都有一个构造函数constructor,当使用new操作符时,就会调用这个构造函数
注:不论有没有写这个constructor,创建实例时都会默认执行一次
class People {
constructor(name, age) {
this.name = name
this.age = age
}
}
new操作,创建实例
const p1 = new People("xiaoming", 22)
//具体过程:
//1.创建一个{}
//2.{}.__proto__ = People.prototype
//3.this = {}
//4.执行函数体
//5.如果构造函数没有返回非空对象,则返回创建出来的新对象
4、类定义方法
//定义方法就直接在类中定义
class People {
constructor(name) {
this.name = name
}
eating() {
console.log(this.name, "正在吃饭")
}
}
事实上,这样子添加的方法最终会被放在People的显示原型上
console.log(Object.getOwnPropertyDescriptors(People.prototype))
//输出:constructor、eating
const p1 = new People("xiaoming")
const p2 = new People("xiaohong")
p1.eating()
p2.eating()
Object.getOwnPropertyDescriptors()方法用来获取一个对象的所有自身属性的描述符
5、类的访问器方法?
在类中也可以创建访问器方法(get、set),对获取属性、修改属性做一层拦截
class People {
constructor() {
this._address = "广州市"
}
get address() {
// 拦截访问操作
return this._address
}
set address(newValue) {
// 拦截设置操作
this._address = newValue
}
}
const p = new People()
console.log(p.address)
p.address = 'xxx'
6、类的静态方法
类的静态方法也称为类方法
普通的方法,是通过实例直接访问,如p1.eating()
类的静态方法,通过类名直接访问,如People.createPerson(),没有必要创建实例
class People {
static createPerson() {}
}
//访问:People.createPerson()
//案例:随机姓名,随机年龄
class People {
constructor(name, age) {
this.name = name
this.age = age
this._address = "广州市"
}
// 类的静态方法
static randomPerson() {
const names = ['吴彦祖','张家辉','彭于晏','胡歌','黎明','刘德华']
var nameIndex = Math.floor(Math.random() * names.length)
var name = names[nameIndex]
var age = Math.floor(Math.random() * 100)
return new People(name, age)
}
}
//调用
for(let i = 0; i < 50; i++) {
console.log(People.randomPerson());
}
静态方法与普通方法的另外一个区别:静态方法存放在类上(所以可以直接调用),普通方法存放在类的显示原型上
3.2 class的继承
7、类的继承
ES6中新增了extends关键字,可以方便我们实现继承
在js中类只能有一个父类:单继承
class Person {
constructor() {}
}
//继承
class Student extends Person() {}
js引擎在解析子类的时候有要求,如果我们有实现继承,那么子类的构造方法中,在使用this之前,必须调用super
1、调用父类的属性
//Person(父类)
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
}
//Student(子类)
class Student extends Person {
constructor(name, age ,sno) {
super(name, age)
this.sno = sno
}
}
//创建实例
var stu = new Student("ming", 19, 2323123)
console.log(stu) //输出:Student { name: 'ming', age: 19, sno: 2323123 }
2、调用父类的方法
如果想要在父类方法的基础上进行重写,没有必要整个重写,而是可以从父类继承一些代码逻辑下来
class Person {
//父类的方法
personMethod() {
console.log("逻辑1");
console.log("逻辑2");
console.log("逻辑3");
}
}
class Student extends Person {
//重写
personMethod() {
super.personMethod() //复用原来的逻辑
console.log("逻辑4");
console.log("逻辑5");
}
}
var stu = new Student()
stu.personMethod() //输出:逻辑1、逻辑2、逻辑3、逻辑4、逻辑5
3.3 ES5代码转化
前面提到,class本质上是构造函数的语法糖。最终会被babel转化为浏览器能够认识的低版本代码(一般是ES5代码)
转换方式:https://babeljs.io/
1、基本的转换
//转换前
class Person {}
//转换后
"use strict";
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Person = function Person() {
_classCallCheck(this, Person);
};
上边的ES5代码,为了防止用户这样子使用类People()——将类作为函数直接调用
这种情况下,会调用_classCallCheck(this, Person),此时this为window/undefined,那么会直接抛出错误
假设去掉这一层判断,class转换后的代码跟一个正常的构造函数没有区别
function Person() { }
2、对属性方法的转换
//转换前
class Person {
constructor(name, age) {
this.name = name
this.age = age
}
eating() {
console.log(this.name, "正在吃饭");
}
}
//转换后
"use strict";
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
//最终定义了该方法
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
//普通的方法,定义在原型上
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
//静态的方法,定义在类上
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
var Person = /*#__PURE__*/ (function () {
function Person(name, age) {
_classCallCheck(this, Person);
//属性直接添加即可
this.name = name;
this.age = age;
}
//调用_createClass,添加方法
(Person, [
{
key: "eating",
value: function eating() {
console.log(this.name, "正在吃饭");
}
}
]);
return Person;
})();
以前我们需要在Person.prototype上添加eating方法,这里同样是这样做,只不过换了写法
调用_createClass,判断是普通方法,调用_defineProperties(person原型,需要添加的方法),并使用Object.defineProperty定义了这个方法
3、继承的转换
由于babel转换后,代码量过多,这里不直接展示
结论:class继承的实现,转换为ES5,实际上利用了寄生组合式继承的方法
