0%

JS 中的设计模式

解决一些特定问题的方法,并总结起来,给一个名字。

定义

在面向对象软件设计过程中针对特定的问题简洁而优雅的解决方案。

原则

找出程序中变化的地方,并将变化封装起来

鸭子类型

如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。

多态

同一操作作用于不同的对象上面,可以产生不同的解释和不同的执行结果。换句话说,给不同的对象发送同一个消息的时候,这些对象会根据这个消息分别给出不同的反馈。

1
2
3
4
5
6
7
8
9
10
11
class Duck {}
class Chicken {}
const makeSound = function (animal) {
if (animal instanceof Duck) {
console.log("嘎嘎嘎");
} else if (animal instanceof Chicken) {
console.log("咯咯咯");
}
};
makeSound(new Duck()); // 嘎嘎嘎
makeSound(new Chicken()); // 咯咯咯
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const makeSound = function (animal: Animal) {
animal.sound();
};
class Animal {
sound() {
throw new Error("不存在sound");
}
}
class Duck extends Animal {
sound() {
console.log("嘎嘎嘎");
}
}
class Chicken extends Animal {
sound() {
console.log("嘎嘎嘎");
}
}
makeSound(new Duck()); // 嘎嘎嘎
makeSound(new Chicken()); // 咯咯咯
// 更多...
// class Chicken extends Animal {
// sound() {
// console.log("嘎嘎嘎");
// }
// }

多态最根本的作用就是通过把过程化的条件分支语句转化为对象的多态性,从而消除这些条件分支语句

封装

考虑你的设计中哪些地方可能变化,这种方式与关注会导致重新设计的原因相反。它不是考虑什么时候会迫使你的设计改变,而是考虑你怎样才能够在不重新设计的情况下进行改变。这里的关键在于封装发生变化的概念,这是许多设计模式的主题

原型模式

基于原型链

单例模式

定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点

场景

  • 需要频繁实例化然后销毁的对象
  • 创建对象时耗时过多或者耗资源过多,但又经常用到的对象
  • 有状态的工具类对象
  • 繁访问数据库或文件的对象

应用

  • window 对象
  • 日志文件
  • 应用配置
  • 线程池

代理模式

定义

代理模式是为一个对象提供一个代替品或占位符,以便控制对他的访问

场景

  • 明星的经纪人
  • 生产数据库访问
  • 翻墙

迭代器模式

定义

顺序访问一个聚合对象的元素,而不需要暴露对象的内部表示。

基本不需要自己实现,大部分语言内置了迭代器

场景

  • Array.prototype.forEach

发布订阅模式

定义

对象间的一对多的依赖关系,当一个对象的状态发生改变时,所以依赖于他的对象都能接受到通知

场景

  • JS 事件模型

命令模式

定义

命令是对命令的封装,每一个命令都是一个操作,请求方发出请求,接收方接收请求,并执行操作。命令模式解耦了请求方和接收方,命令模式属于行为型模式

场景

  • 处理一些功能,但是不知道请求的接收者是谁,操作是什么,希望以一种松耦合的方式来设计程序。
  • JS 高阶函数

组合模式

定义

组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结

场景

  • 树形菜单
  • 文件
  • 文件夹的管理

模板模式

定义

在模板模式(Template Pattern)中,一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。这种类型的设计模式属于行为型模式。

场景

  • 造房子
  • 泡茶还是泡咖啡

策略模式

定义一系列的算法,把它们一个个封装起来,并且使它们可以相互替换。

计算绩效

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const calculateBonus = function (
performanceLevel: "S" | "A" | "B",
salary: number
) {
if (performanceLevel === "S") {
return salary * 4;
}
if (performanceLevel === "A") {
return salary * 3;
}
if (performanceLevel === "B") {
return salary * 2;
}
};
calculateBonus("B", 20000); // 输出:40000

// ts实现
const bounsMap = new Map([
["S", (salary: number) => salary * 4],
["A", (salary: number) => salary * 3],
["B", (salary: number) => salary * 2],
]);
bounsMap.get("S")?.(1000);

// 完整实现
enum strategyEnum {
S = "S",
A = "A",
B = "B",
}
const bounsMap = new Map<strategyEnum, { times: number }>([
[strategyEnum.S, { times: 4 }],
[strategyEnum.B, { times: 3 }],
[strategyEnum.B, { times: 2 }],
]);

const getBouns = (strategy: strategyEnum, salary: number) => {
const bouns = bounsMap.get(strategy);
if (!bouns) {
return 0;
}
return bouns.times * salary;
};
getBouns(strategyEnum.S, 1000);

位 1 的个数

题目

编写一个函数,输入是一个无符号整数(以二进制串的形式),返回其二进制表达式中数字位数为 ‘1’ 的个数(也被称为汉明重量)。

提示:

  • 请注意,在某些语言(如 Java)中,没有无符号整数类型。在这种情况下,输入和输出都将被指定为有符号整数类型,并且不应影响您的实现,因为无论整数是有符号的还是无符号的,其内部的二进制表示形式都是相同的。
  • 在 Java 中,编译器使用二进制补码记法来表示有符号整数。因此,在上面的 示例 3 中,输入表示有符号整数 -3。

示例 1:

输入:00000000000000000000000000001011

输出:3

解释:输入的二进制串 00000000000000000000000000001011 中,共有三位为 ‘1’。

示例 2:

输入:00000000000000000000000010000000

输出:1

解释:输入的二进制串 00000000000000000000000010000000 中,共有一位为 ‘1’。

示例 3:

输入:11111111111111111111111111111101

输出:31

解释:输入的二进制串 11111111111111111111111111111101 中,共有 31 位为 ‘1’。

提示:

输入必须是长度为 32 的 二进制串 。

进阶:

如果多次调用这个函数,你将如何优化你的算法?

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 自己写的 0ms 2mb
impl Solution {
pub fn hammingWeight (n: u32) -> i32 {
let mut count = 0;
let mut i = 1;
while i <= n && i != 0 {
if n & i != 0 {
count += 1;
}
i = i << 1;
}
count
}
}
1
2
3
4
5
6
7
8
9
10
11
// 构造32位蒙版(只有一个1),进行与操作
impl Solution {
pub fn hammingWeight (n: u32) -> i32 {
let (mut ret, mut mask) = (0, 1u32);
for i in 0..32 {
if n & mask != 0 { ret += 1; }
mask = mask << 1;
}
ret
}
}
1
2
3
4
5
6
// count_one 方法 可以直接获取数量
impl Solution {
pub fn hammingWeight (n: u32) -> i32 {
n.count_ones() as i32
}
}

思路

  • 位的与方法判断是否是 1,是的话,数量加 1
  • 位移的方式变更 1 的位置
  • 位移可能会越界,超出最大值的时候位移数据就是 0,需要注意一下
  • 别人的操作是真的牛皮

相关连接

最长公共前缀

编写一个函数来查找字符串数组中的最长公共前缀。

如果不存在公共前缀,返回空字符串 “”。

示例 1:

输入:strs = [“flower”,”flow”,”flight”]

输出:”fl”

示例 2:

输入:strs = [“dog”,”racecar”,”car”]

输出:””

解释:输入不存在公共前缀。

提示:

  • 1 <= strs.length <= 200
  • 0 <= strs[i].length <= 200
  • strs[i] 仅由小写英文字母组成

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
impl Solution {
pub fn longest_common_prefix(strs: Vec<String>) -> String {
let mut result = String::from("");
let mut index = 0;
'looper: loop {
let mut _temp = "";
for str in &strs {
if str.len() == 0 {
break 'looper;
}
if index >= str.len() {
break 'looper;
}
let c_val = &str[index..index + 1];
if c_val == "" {
break 'looper;
}
if _temp == "" {
_temp = c_val;
continue;
}
if _temp != c_val {
break 'looper;
}
_temp = c_val;
}
result.push_str(&strs.get(0).unwrap()[index..index + 1]);
index += 1;
}
result
}
}

解释

  • 找出所有字符串的第{index}个元素,判断是否全部相等。
  • 如果相等放入结果中,继续循环
  • 不相等跳出外层循环,返回结果
  • 0ms,2.1mb

来源

JS 中的数据结构

数组

优点

  • 原型方法较多,可以比较方便的实现各种功能
  • JSON 支持
  • 相比 java,数组的长度不是固定的,不存在越界问题
  • 兼容性好

缺点

  • 没有获取到索引的情况下,查询效率相对低,时间复杂度为 O(n)
  • 插入数据效率低O(n)
  • 删除数据效率O(n)

应用场景

  • 绝大部分数据处理
  • JSON 支持,可以直接在接口中返回
  • 前端列表渲染

对象

优点

  • 兼容性好
  • 查询效率高,时间复杂度为 O(1)
  • JSON 支持
  • 相对 Map 可以继承原型链上的属性

缺点

  • 相对 MAP 键值只能是 string
  • 相对 MAP 顺序不确定
  • 相对 MAP 没有迭代属性

应用场景

  • 通用场景
  • 需要一个固定 key 来做区分数据内容

MAP

优点

  • 查询效率高,时间复杂度为 O(1)
  • 相对对象这种结构,数据更清晰,效率更高
  • 相对 OBJECT,语义化更好,支持功能多

缺点

  • 原型方法没有数组多
  • JSON 不支持,如果作为接口数据返回,需要重新转换为数组或对象

应用场景

  • 需要一个固定格式的 key value

链表

优点

  • 插入性能比数组高 O(1)
  • 删除性能比数组高 O(1)

缺点

  • 索引效率低 O(n)
  • 原生不支持,需要自己实现链表结构

应用场景

  • 动态插入与删除比较多的场景

应用场景

  • 先进后出
  • JS 执行顺序

队列

应用场景

  • 先进先出
  • 排队场景
  • 性能瓶颈的时候,使用队列,排队处理数据

位图

应用场景

  • 打标签的场景

应用场景

  • DOM 树、CSS 树
  • 多层JSON
  • 级联菜单

尝试着去分享更多的东西

起码留下点什么

加油

为众人抱薪者,不可使其冻毙于风雪;

为大众谋福利者,不可使其孤军奋战;

为自由开路者,不可使其困顿于荆棘。

如果天空是黑暗的,那就摸黑生存;如果发出声音是危险的,那就保持沉默;如果自觉无力发光,那就蜷伏于墙角。但是,不要习惯了黑暗就为黑暗辩护,也不要为自己的苟且而得意,不要嘲讽那些比自己更勇敢的人。我们可以卑微如尘土,但不可扭曲如蛆虫

原来,有你陪着已经这么开心
房间里,没有你,这么空旷
一边流泪,一边收拾衣服的感觉确实不好
有点怀疑,自己为什么会劝你回到哪里
我果然是个傻逼

一直在身边的,才是最容易被忽视的。
有一天突然不见了。
原来是这么难受。

嗯。

有些不知所措

跟自己想的那种情况 不太一样

哭成了傻逼

我以为我能很平静的接受这件事

回到家里,看着熟悉的房间,总是感觉她在那里

还没有离开

一边写 一边哭

不写了

外观数列

题目

给定一个正整数 n ,输出外观数列的第 n 项。

「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。

你可以将其视作是由递归公式定义的数字字符串序列:

  • countAndSay(1) = “1”
  • countAndSay(n) 是对 countAndSay(n-1) 的描述,然后转换成另一个数字字符串。
    前五项如下:
    1. 1
    2. 11
    3. 21
    4. 1211
    5. 111221

    第一项是数字 1

    描述前一项,这个数是 1 即 “ 一 个 1 ”,记作 “11”

    描述前一项,这个数是 11 即 “ 二 个 1 ” ,记作 “21”

    描述前一项,这个数是 21 即 “ 一 个 2 + 一 个 1 ” ,记作 “1211”

    描述前一项,这个数是 1211 即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ,记作 “111221”

    要 描述 一个数字字符串,首先要将字符串分割为 最小 数量的组,每个组都由连续的最多 相同字符 组成。然后对于每个组,先描述字符的数量,然后描述字符,形成一个描述组。要将描述转换为数字字符串,先将每组中的字符数量用数字替换,再将所有描述组连接起来。
  • 示例 1:

输入:n = 1

输出:”1”

解释:这是一个基本样例。

  • 示例 2:

输入:n = 4

输出:”1211”

解释:

countAndSay(1) = “1”

countAndSay(2) = 读 “1” = 一 个 1 = “11”

countAndSay(3) = 读 “11” = 二 个 1 = “21”

countAndSay(4) = 读 “21” = 一 个 2 + 一 个 1 = “12” + “11” = “1211”

题解

  • 普通做法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function countAndSay(n: number): string {
    const getNextNum = (num: string) => {
    let i = 0;
    let result = "";
    let counter = 1;
    while (i < num.length) {
    if (num[i] === num[i + 1]) {
    counter++;
    } else {
    result += `${counter}${num[i]}`;
    counter = 1;
    }
    i++;
    }
    return result;
    };
    let current = "1";
    for (let i = 1; i < n; i++) {
    current = getNextNum(current);
    }
    return current;
    }
  • 递归做法

    感觉性能会比较差

来源

实现 strStr() 函数。

题目

给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串出现的第一个位置(下标从 0 开始)。如果不存在,则返回 -1 。

说明:

当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。

对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与 C 语言的 strstr() 以及 Java 的 indexOf() 定义相符。

  • 示例 1:

输入:haystack = “hello”, needle = “ll”
输出:2
示例 2:

输入:haystack = “aaaaa”, needle = “bba”
输出:-1
示例 3:

输入:haystack = “”, needle = “”
输出:0

提示:

  • 0 <= haystack.length, needle.length <= 5 * 104
  • haystack 和 needle 仅由小写英文字符组成

题解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 不使用字符串原型方法
function strStr(haystack: string, needle: string): number {
if (needle.length > haystack.length) {
return -1;
}
if (!needle.length) {
return 0;
}
for (let i = 0; i < haystack.length; i++) {
if (haystack[i] !== needle[0]) {
continue;
}
let temp = false;
for (let j = 0; j < needle.length; j++) {
if (haystack[j + i] !== needle[j]) {
temp = false;
break;
}
temp = true;
}
if (temp) {
return i;
}
}
return -1;
}
// 原生方法
function strStr(haystack: string, needle: string): number {
return haystack.indexOf(needle);
}

来源

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/top-interview-questions-easy/xnr003/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

验证回文数

题目

给定一个字符串,验证它是否是回文串,只考虑字母和数字字符,可以忽略字母的大小写。

说明:本题中,我们将空字符串定义为有效的回文串。

  • 示例 1:

输入: “A man, a plan, a canal: Panama”
输出: true
解释:”amanaplanacanalpanama” 是回文串

  • 示例 2:

输入: “race a car”
输出: false
解释:”raceacar” 不是回文串

提示:

  • 1 <= s.length <= 2 * 105
  • 字符串 s 由 ASCII 字符组成

答案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function isPalindrome(s: string): boolean {
let leftIndex = 0;
let rightIndex = s.length - 1;
const lowS = s.toLowerCase();
const isSmaller = (s: string) =>
(s.charCodeAt(0) >= 97 && s.charCodeAt(0) <= 122) ||
(s.charCodeAt(0) >= 48 && s.charCodeAt(0) <= 57);
while (leftIndex < rightIndex) {
if (!isSmaller(lowS[leftIndex])) {
leftIndex++;
continue;
}
if (!isSmaller(lowS[rightIndex])) {
rightIndex--;
continue;
}
if (lowS[leftIndex] !== lowS[rightIndex]) {
return false;
} else {
leftIndex++;
rightIndex--;
}
}
return true;
}

版权声明