本例结合ES6面向对象语法、设计模式、闭包,完成一个附近商家的自动推荐功能;
案例需求如下:
√ 封装一个地理位置类Location,属性x,y, √ 求距离方法:getDistance(anotherLocation)可以计算与另一个地理位置的直线距离 √ 随意造两个地理位置实例,并求它们之间的距离 √ 创建商家类Shop,继承自地理位置类,同时拥有类别属性type、单价属性price,和服务方法serve(),留白等待子类去做具体实现 √ 实现饭馆类Restaurant、超市类Supermarket、酒店类Hotel,全部继承Shop,并覆写服务方法serve() √ 实现Location的子类消费者Consumer,拥有name、money属性,和消费方法consume() √ 产生一个随机位置的消费者和100个位置类型都随机的商家 √ 写一个推荐函数recommend(consumer,type,n),为消费者提供n个离他位置最近的商家 √ 要求用构造函数和类两种方式实现 √ 让消费者按照推荐去这n家商家循环消费,直到耗尽money为止,留下每家商家的消费记录(选做/尝试用闭包)
封装地理位置类 我们使用随机算法生成随机的地理位置,代码实现如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
/* 封装一个数学工具 */
class Mathtool {
// 该工具不需要任何的天然属性,此处省略constructor,事实上会默认继承Object
getRandom(a,b){
return a + parseInt(Math.random()*(b-a+1))
}
}
// 单例模式 工具类/服务类的实例 全局共享一个就OK 节约内存
/* 获得MathTool的全局唯一单例 singleton() getMathtoolSingleton() */
let mathTool
function getMathtoolSingleton() {
if(!mathTool){
mathTool = new Mathtool()
}
return mathTool
}
class Location {
constructor(x,y){
// this.x = x || new MathTool().getRandom(-1000,1000)
// this.y = y || new MathTool().getRandom(-1000,1000)
this.x = x || getMathtoolSingleton().getRandom(-1000,1000)
this.y = y || getMathtoolSingleton().getRandom(-1000,1000)
}
}
</script>
<script>
const loc = new Location()
console.log(loc);
</script>
</body>
</html>
地理位置类实现求与另一个地理位置的距离 思路很简单,根据两个地理位置的x,y坐标,通过勾股定理实现求取它们之间的直线距离
class Location {
constructor(x, y) {
// this.x = x || new MathTool().getRandom(-1000,1000)
// this.y = y || new MathTool().getRandom(-1000,1000)
this.x = x || getMathtoolSingleton().getRandom(-1000, 1000);
this.y = y || getMathtoolSingleton().getRandom(-1000, 1000);
}
/*
求当前Location实例与另外一个Location实例之间的距离
distance 距离 another 另外一个 location 地理位置
*/
getDistance(anotherLocation) {
return Math.sqrt(
Math.pow(anotherLocation.x - this.x, 2) +
Math.pow(anotherLocation.y - this.y, 2)
).toFixed(2)
}
/* 覆写toString()方法 让打印精简又漂亮 */
toString() {
return `(${this.x},${this.y})`
}
}
封装商家和消费者类
- 它们都继承于地理位置类,因此可以很方便地通过getDistance方法实现对商家与消费者物理距离的计算;
- 商家类预留出serve抽象方法,等待具体商家的子类去做具体实现
- 消费者实现在某个商家进行消费的方法
// 商家类
class Shop extends Location {
constructor(type,name){
super()
this.type = type
this.name = name
}
toString(){
return `<${this.name}_(${this.x},${this.y})>`
}
//消费抽象方法
serve(){}
}
/* 消费者类 */
class Consumer extends Location {
// 在new Consumer()时会调用Consumer的constructor
// constructor()是function Consumer()的语法糖 是类同名的构造函数的语法糖
constructor(name, money) {
// 调用父类的构造函数 Location() 完成x,y的默认赋值工作(传参了就用实参 否则用随机数)
super();
// 子类自己的属性接收
this.name = name;
this.money = money;
}
// 父类的方法默认已经被继承 extends语法糖默认帮你做的事情
// 只需要写子类自己的独有方法
// consume 消费
consume(shop) {
const price = shop.price;
this.money -= price;
// 返回本次消费的账单
return `${this.name}在${shop.toString()}消费了${price}元,实时余额为${
this.money
}`;
}
}
封装一些商家的具体实现类
- 主要实现不同的服务方法serve,达到多态的效果
- 此处封装出超市、酒店、三种子类:
/* 饭馆 */
class Restaurant extends Shop {
constructor(name,price){
// 调用父类的构造器方法
// 一行三行:super()继承Location的属性 this.type = type this.name = name 绑定类别和名称到当前的Restaurant实例
super(TYPE_RESTAURANT,name)
this.price = price || getMathtoolSingleton().getRandom(5,1000)
}
serve(){
console.log(`${this.name}丰盛可口不拉肚子欢迎品尝`);
}
}
/* 超市 */
class Supermarket extends Shop {
constructor(name,price){
// 调用父类的构造器方法
// 一行三行:super()继承Location的属性 this.type = type this.name = name 绑定类别和名称到当前的Restaurant实例
super(TYPE_SUPERMARKET,name)
this.price = price || getMathtoolSingleton().getRandom(0,1000)
}
serve(){
console.log(`${this.name}物美价廉速来消费`);
}
}
/* 宾馆 */
class Hotel extends Shop {
constructor(name,price){
// 调用父类的构造器方法
// 一行三行:super()继承Location的属性 this.type = type this.name = name 绑定类别和名称到当前的Restaurant实例
super(TYPE_HOTEL,name)
this.price = price || getMathtoolSingleton().getRandom(50,5000)
}
serve(){
console.log(`${this.name}四季如春给您家的感觉`);
}
}
生成随机商家
- 这里商家的位置和店铺名称都使用随机算法
- 店铺的名称使用【随机形容词+随机连词+随机形容词】的方式,生成形如【好又多超市】的店铺名称
// 生成100个随机商家
const shops = [];
for (let i = 0; i < 100; i++) {
// 随机生成一个类型
const key = getMathtoolSingleton().getRandom(1, 3);
// 查询得到一个【构造函数】 例如:1 Restaurant
const constructor = shopConstructorObj[key];
// console.log(constructor);
// new Restaurant(根据种类号随机店名)
const shop = new constructor(getMathtoolSingleton().getRandomShopname(key));
shops.push(shop);
}
实现了生成随机店名的Mathtool代码如下:
/* 封装一个数学工具 */
class Mathtool {
constructor(){
// 一堆形容词
this.adjs = "大小多少快慢好坏大小贫富贵贱贤愚聪笨东西南北高矮胖肥瘦美丑善恶强弱帅矬黑白"
// 一堆连词
this.conjs = "又且而尔并特更"
// 随机店名= 一个随机形容词 + 一个随机连词 + 一个随机形容词 => 好又多
}
// 该工具不需要任何的天然属性,此处省略constructor,事实上会默认继承Object
getRandom(a, b) {
return a + parseInt(Math.random() * (b - a + 1));
}
/* 根据类型号1 2 3 获得一个随机店名 */
getRandomShopname(type) {
let name =
// 随机取一个形容词
this.adjs[this.getRandom(0,this.adjs.length-1)]
// 随机取一个连词
+ this.conjs[this.getRandom(0,this.conjs.length-1)]
// 随机取一个形容词
+ this.adjs[this.getRandom(0,this.adjs.length-1)]
// 根据类型不同 拼接一个后缀
switch (type) {
case TYPE_RESTAURANT:
name += "大食堂"
break;
case TYPE_SUPERMARKET:
name += "大卖场"
break;
case TYPE_HOTEL:
name += "大酒店"
break;
default:
break;
}
return name
}
}
封装附近的服务
- 该服务在初始化时会生成100个位置、种类、店名都随机的商家实例;
- 根据给定的消费者实例、店铺类型、店铺数量,实现为消费者推荐距离他最近的n家店铺;
- 该服务在全局只有一个单例 实现代码 如下:
// 封装一个服务管理类 NearbyService 附近的服务
// 注册商家 注销商家 推荐商家 导航(在消费者和具体商家之间牵线搭桥 形成一个闭包 消费记录保存在该闭包中)
class NearbyService {
constructor() {
// 全部商家全图
this.allShops = [];
// 根据类型查询构造函数的数据结构
// 键是1,2,3 值是构造函数/类
this.shopConstructorObj = {
1: Restaurant, //饭馆
2: Supermarket, //超市
3: Hotel, //宾馆
};
// 初始化:生成全部商家
this.init();
}
init() {
for (let i = 0; i < 100; i++) {
// 随机生成一个类型
const key = getMathtoolSingleton().getRandom(1, 3);
// 查询得到一个【构造函数】 例如:1 Restaurant
const constructor = this.shopConstructorObj[key];
// console.log(constructor);
// new Restaurant(根据种类号随机店名)
const shop = new constructor(
getMathtoolSingleton().getRandomShopname(key)
);
this.allShops.push(shop);
}
// console.log(this.allShops);
}
/*
推荐
consumer 消费者
type 要过滤的商家类型
n 推荐最近的n个
*/
recommend(consumer, type, n) {
// 按照type过滤
const tempShops = this.allShops.filter(item=>item.type===type)
// 准备一个用于排序的数组【商店的距离们】 [ [商店1,距离1],[商店2,距离2]... ]
let shopDistances = []
// 计算所有商家与消费者的直线距离
tempShops.forEach(shop=>{
// 计算所有商家与消费者的直线距离
const distance = consumer.getDistance(shop)
// 按照[商店1,距离1]的格式 将数据丢入shopDistances待排序
shopDistances.push([shop,distance])
})
// 按距离排序 a[商店a,距离a] b[商店b,距离b]
// 排序规则 检测 【a.距离 VS b.距离】 => 【a[1] VS b[1]】
shopDistances = shopDistances.sort((a,b)=>a[1]-b[1])
// 截取前n项 形成一个子数组 并返回
return shopDistances.slice(0,n)
}
}
// NearbyService 附近的服务 给任何人推荐算法都一样 使用单例模式 instance 实例
let nbServiceInstance;
// 获取NearbyService的单例
function getNbSerbiceSingleton(params) {
if (!nbServiceInstance) {
nbServiceInstance = new NearbyService();
}
return nbServiceInstance;
}
保存每个商家的消费记录
- 我们在NearbyService中实现为消费者导航到某家商店的服务,该方法返回一个接口闭包函数;
- 消费者调用该接口函数即可实现在店内消费,并将不同商店的历史消费记录保存在各自独立的闭包中; 导航服务实现:
/* 导航 返回一个该消费者在该店消费的接口函数*/
navigateTo(consumer,shop){
// 该消费者在该店内消费的历史详单
// 消费记录 consume 消费 histories 历史们
// ==============闭包数据================
const consumeHistories = []
// ======================================
// 返回一个该消费者在该店消费的接口函数
return function () {
// 用户在商家消费 得到本次账单
const history = consumer.consume(shop)
// 本次账单丢入在该店消费的历史账单 闭包数据
consumeHistories.push(history)
// 直接返回最新的完整历史账单
return consumeHistories
}
}
测试导航与消费
// 定义商家的类型常量
const TYPE_RESTAURANT = 1; //饭馆
const TYPE_SUPERMARKET = 2; //超市
const TYPE_HOTEL = 3; //宾馆
// // 根据类型查询构造函数的数据结构
// // 键是1,2,3 值是构造函数/类
// const shopConstructorObj = {
// 1: Restaurant, //饭馆
// 2: Supermarket, //超市
// 3: Hotel, //宾馆
// };
function testLocation() {
const loc1 = new Location();
const loc2 = new Location();
console.log(loc1.toString(), loc2.toString());
console.log(loc1.getDistance(loc2));
}
function testShop() {
const r = new Restaurant("老王大食堂");
const s = new Supermarket("老王超市");
const h = new Hotel("老王招待所");
console.log(r.toString());
console.log(s.toString());
console.log(h.toString());
// r.serve()
// s.serve()
// h.serve()
const shops = [r, s, h];
shops.forEach((shop) => shop.serve());
}
// 测试消费者
function testConsumer() {
const c = new Consumer("张双蛋", 10000);
const r = new Restaurant("老王大食堂");
// 消费并得到返回的消费记录
const record = c.consume(r);
console.log(record);
}
const nbService = getNbSerbiceSingleton()
// 生成一个用户
const c = new Consumer("张双蛋",100000)
// 为蛋哥推荐离他最近的10家饭店
// [ [好又多,10m] [好友少,20m] ]
const rShopInfos = nbService.recommend(c,TYPE_RESTAURANT,10)
// console.log(rShops);
// 生成这些点的消费接口函数 存入一个obj{ shop1:消费接口函数1, shop2:消费接口函数2 }
const shopApis = {}
rShopInfos.forEach(shopInfo=>{
const shop = shopInfo[0]
// 生成每家店的消费接口函数
shopApi = nbService.navigateTo(c,shop)
// 以商店为键 该店的消费接口为值 保存数据
shopApis[shop] = shopApi
})
// 准备存储所有店的消费历史 { 商店1:该店的历史详单, 商店2:该店的历史详单 ...}
const shopHistories = {}
// 循环在推荐的店内消费 直到成为一个穷光蛋为止
while(c.money > 0){
// 在10家里中随机生成一个序号
const num = getMathtoolSingleton().getRandom(0,9)
// 拿到序号对应的店铺距离信息 [好又多,10m] 再进一步拿到商店 [好又多,10m][0]
const shop = rShopInfos[num][0]
// 查询得到去该店消费的接口函数
const shopApiFn = shopApis[shop]
// 调用接口函数 并获得在该店消费的最新(latest)完整历史记录
const latestHistories = shopApiFn()
// 将在该店的最新消费记录存起来
shopHistories[shop] = latestHistories
}
console.log(shopHistories);
执行结果如图,每个商家的消费记录都被各自的闭包完整记录
案例完整代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script>
/* 封装一个数学工具 */
class Mathtool {
constructor() {
// 一堆形容词
this.adjs =
"大小多少快慢好坏大小贫富贵贱贤愚聪笨东西南北高矮胖肥瘦美丑善恶强弱帅矬黑白";
// 一堆连词
this.conjs = "又且而尔并特更";
// 随机店名= 一个随机形容词 + 一个随机连词 + 一个随机形容词 => 好又多
}
// 该工具不需要任何的天然属性,此处省略constructor,事实上会默认继承Object
getRandom(a, b) {
return a + parseInt(Math.random() * (b - a + 1));
}
/* 根据类型号1 2 3 获得一个随机店名 */
getRandomShopname(type) {
let name =
// 随机取一个形容词
this.adjs[this.getRandom(0, this.adjs.length - 1)] +
// 随机取一个连词
this.conjs[this.getRandom(0, this.conjs.length - 1)] +
// 随机取一个形容词
this.adjs[this.getRandom(0, this.adjs.length - 1)];
// 根据类型不同 拼接一个后缀
switch (type) {
case TYPE_RESTAURANT:
name += "大食堂";
break;
case TYPE_SUPERMARKET:
name += "大卖场";
break;
case TYPE_HOTEL:
name += "大酒店";
break;
default:
break;
}
return name;
}
}
// 单例模式 工具类/服务类的实例 全局共享一个就OK 节约内存
/* 获得MathTool的全局唯一单例 singleton() getMathtoolSingleton() */
let mathTool;
function getMathtoolSingleton() {
if (!mathTool) {
mathTool = new Mathtool();
}
return mathTool;
}
class Location {
constructor(x, y) {
// this.x = x || new MathTool().getRandom(-1000,1000)
// this.y = y || new MathTool().getRandom(-1000,1000)
this.x = x || getMathtoolSingleton().getRandom(-1000, 1000);
this.y = y || getMathtoolSingleton().getRandom(-1000, 1000);
}
/*
求当前Location实例与另外一个Location实例之间的距离
distance 距离 another 另外一个 location 地理位置
*/
getDistance(anotherLocation) {
return Math.sqrt(
Math.pow(anotherLocation.x - this.x, 2) +
Math.pow(anotherLocation.y - this.y, 2)
).toFixed(2);
}
/* 覆写toString()方法 让打印精简又漂亮 */
toString() {
return `(${this.x},${this.y})`;
}
}
</script>
<script>
class Shop extends Location {
constructor(type, name) {
super();
this.type = type;
this.name = name;
}
toString() {
return `<${this.name}_(${this.x},${this.y})>`;
}
// 服务
serve() { }
getPrice() { }
}
class Restaurant extends Shop {
constructor(name, price) {
// 调用父类的构造器方法
// 一行三行:super()继承Location的属性 this.type = type this.name = name 绑定类别和名称到当前的Restaurant实例
super(TYPE_RESTAURANT, name);
this.price = price || getMathtoolSingleton().getRandom(5, 1000);
}
serve() {
console.log(`${this.name}丰盛可口不拉肚子欢迎品尝`);
}
getPrice() {
return getMathtoolSingleton().getRandom(5, 1000);
}
}
class Supermarket extends Shop {
constructor(name, price) {
// 调用父类的构造器方法
// 一行三行:super()继承Location的属性 this.type = type this.name = name 绑定类别和名称到当前的Restaurant实例
super(TYPE_SUPERMARKET, name);
this.price =
price || getMathtoolSingleton().getRandom(0, 1000);
}
serve() {
console.log(`${this.name}物美价廉速来消费`);
}
getPrice() {
return getMathtoolSingleton().getRandom(0, 1000);
}
}
class Hotel extends Shop {
constructor(name, price) {
// 调用父类的构造器方法
// 一行三行:super()继承Location的属性 this.type = type this.name = name 绑定类别和名称到当前的Restaurant实例
super(TYPE_HOTEL, name);
this.price =
price || getMathtoolSingleton().getRandom(50, 5000);
}
serve() {
console.log(`${this.name}四季如春给您家的感觉`);
}
getPrice() {
return getMathtoolSingleton().getRandom(50, 5000);
}
}
/* 消费者类 */
class Consumer extends Location {
// 在new Consumer()时会调用Consumer的constructor
// constructor()是function Consumer()的语法糖 是类同名的构造函数的语法糖
constructor(name, money) {
// 调用父类的构造函数 Location() 完成x,y的默认赋值工作(传参了就用实参 否则用随机数)
super();
// 子类自己的属性接收
this.name = name;
this.money = money;
}
// 父类的方法默认已经被继承 extends语法糖默认帮你做的事情
// 只需要写子类自己的独有方法
// consume 消费
consume(shop) {
const price = shop.getPrice();
this.money -= price;
// 返回本次消费的账单
return `${this.name}在${shop.toString()}消费了${price}元,实时余额为${this.money}`;
}
}
</script>
<!-- NearbyService -->
<script>
// 封装一个服务管理类 NearbyService 附近的服务
// 注册商家 注销商家 推荐商家 导航(在消费者和具体商家之间牵线搭桥 形成一个闭包 消费记录保存在该闭包中)
class NearbyService {
constructor() {
// 全部商家全图
this.allShops = [];
// 根据类型查询构造函数的数据结构
// 键是1,2,3 值是构造函数/类
this.shopConstructorObj = {
1: Restaurant, //饭馆
2: Supermarket, //超市
3: Hotel, //宾馆
};
// 初始化:生成全部商家
this.init();
}
init() {
for (let i = 0; i < 100; i++) {
// 随机生成一个类型
const key = getMathtoolSingleton().getRandom(1, 3);
// 查询得到一个【构造函数】 例如:1 Restaurant
const constructor = this.shopConstructorObj[key];
// console.log(constructor);
// new Restaurant(根据种类号随机店名)
const shop = new constructor(
getMathtoolSingleton().getRandomShopname(key)
);
this.allShops.push(shop);
}
// console.log(this.allShops);
}
/*
推荐
consumer 消费者
type 要过滤的商家类型
n 推荐最近的n个
返回值 [ [商店1,距离1] ... ]
*/
recommend(consumer, type, n) {
// 按照type过滤
const tempShops = this.allShops.filter(item => item.type === type)
// 准备一个用于排序的数组【商店的距离们】 [ [商店1,距离1],[商店2,距离2]... ]
let shopDistances = []
// 计算所有商家与消费者的直线距离
tempShops.forEach(shop => {
// 计算所有商家与消费者的直线距离
const distance = consumer.getDistance(shop)
// 按照[商店1,距离1]的格式 将数据丢入shopDistances待排序
shopDistances.push([shop, distance])
})
// 按距离排序 a[商店a,距离a] b[商店b,距离b]
// 排序规则 检测 【a.距离 VS b.距离】 => 【a[1] VS b[1]】
shopDistances = shopDistances.sort((a, b) => a[1] - b[1])
// 截取前n项 形成一个子数组 并返回
return shopDistances.slice(0, n)
}
/* 导航 返回一个该消费者在该店消费的接口函数*/
navigateTo(consumer, shop) {
// 该消费者在该店内消费的历史详单
// 消费记录 consume 消费 histories 历史们
// ==============闭包数据================
const consumeHistories = []
// ======================================
// 返回一个该消费者在该店消费的接口函数
return function () {
// 用户在商家消费 得到本次账单
const history = consumer.consume(shop)
// 本次账单丢入在该店消费的历史账单 闭包数据
consumeHistories.push(history)
// 直接返回最新的完整历史账单
return consumeHistories
}
}
}
// NearbyService 附近的服务 给任何人推荐算法都一样 使用单例模式 instance 实例
let nbServiceInstance;
// 获取NearbyService的单例
function getNbSerbiceSingleton(params) {
if (!nbServiceInstance) {
nbServiceInstance = new NearbyService();
}
return nbServiceInstance;
}
</script>
<!-- 测试逻辑 -->
<script>
// 定义商家的类型常量
const TYPE_RESTAURANT = 1; //饭馆
const TYPE_SUPERMARKET = 2; //超市
const TYPE_HOTEL = 3; //宾馆
// // 根据类型查询构造函数的数据结构
// // 键是1,2,3 值是构造函数/类
// const shopConstructorObj = {
// 1: Restaurant, //饭馆
// 2: Supermarket, //超市
// 3: Hotel, //宾馆
// };
function testLocation() {
const loc1 = new Location();
const loc2 = new Location();
console.log(loc1.toString(), loc2.toString());
console.log(loc1.getDistance(loc2));
}
function testShop() {
const r = new Restaurant("老王大食堂");
const s = new Supermarket("老王超市");
const h = new Hotel("老王招待所");
console.log(r.toString());
console.log(s.toString());
console.log(h.toString());
// r.serve()
// s.serve()
// h.serve()
const shops = [r, s, h];
shops.forEach((shop) => shop.serve());
}
// 测试消费者
function testConsumer() {
const c = new Consumer("张双蛋", 1000);
const r = new Restaurant("老王大食堂");
// 消费并得到返回的消费记录
const record = c.consume(r);
console.log(record);
}
const nbService = getNbSerbiceSingleton()
// 生成一个用户
const c = new Consumer("张双蛋", 100000)
// 为蛋哥推荐离他最近的10家饭店
// [ [好又多,10m] [好友少,20m] ]
const rShopInfos = nbService.recommend(c, TYPE_RESTAURANT, 10)
// console.log(rShops);
// 生成这些点的消费接口函数 存入一个obj{ shop1:消费接口函数1, shop2:消费接口函数2 }
const shopApis = {}
rShopInfos.forEach(shopInfo => {
const shop = shopInfo[0]
// 生成每家店的消费接口函数
shopApi = nbService.navigateTo(c, shop)
// 以商店为键 该店的消费接口为值 保存数据
shopApis[shop] = shopApi
})
// 准备存储所有店的消费历史 { 商店1:该店的历史详单, 商店2:该店的历史详单 ...}
const shopHistories = {}
// 循环在推荐的店内消费 直到成为一个穷光蛋为止
while (c.money > 0) {
// 在10家里中随机生成一个序号
const num = getMathtoolSingleton().getRandom(0, 9)
// 拿到序号对应的店铺距离信息 [好又多,10m] 再进一步拿到商店 [好又多,10m][0]
const shop = rShopInfos[num][0]
// 查询得到去该店消费的接口函数
const shopApiFn = shopApis[shop]
// 调用接口函数 并获得在该店消费的最新(latest)完整历史记录
const latestHistories = shopApiFn()
// 将在该店的最新消费记录存起来
shopHistories[shop] = latestHistories
}
console.log(shopHistories);
</script>
</body>
</html>