参考:[https://juejin.cn/post/6844903911686406158]

继承

原型链继承

子类的原型被设置为父类的实例,从而使子类能够访问父类的属性和方法。

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
// 父类构造函数
function Animal(name) {
this.name = name;
this.canWalk = true;
}

// 给父类原型添加方法
Animal.prototype.eat = function() {
console.log(this.name + ' is eating.');
};

// 子类构造函数
function Dog(name, breed) {
this.name = name;
this.breed = breed;
}

// 子类继承父类
Dog.prototype = new Animal();

// 子类特有的方法
Dog.prototype.bark = function() {
console.log(this.name + ' says woof!');
};

var myDog = new Dog('Rex', 'German Shepherd');
myDog.eat(); // Rex is eating.
myDog.bark(); // Rex says woof!

构造函数

1
2
3
4
5
6
function Animal(name){
this.name=name;
this.sex=sex;
}

const dog=new Animal(xiaoba)

寄生继承

寄生式继承是在原型式继承的基础上进行增强,创建一个新的对象并增强它的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function createDog(original) {
var clone = Object.create(original); // 原型式继承
clone.bark = function() {
console.log('Woof!');
};
return clone;
}

var animal = {
canWalk: true,
eat: function() {
console.log('Eating...');
}
};

var dog = createDog(animal);
dog.eat(); // Eating...
dog.bark(); // Woof!

寄生组合式继承(最佳方式)

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 Animal(name) {
this.name = name;
this.canWalk = true;
}

Animal.prototype.eat = function() {
console.log(this.name + ' is eating.');
};

function Dog(name, breed) {
Animal.call(this, name); // 借用构造函数
this.breed = breed;
}

// 寄生组合继承
Dog.prototype = Object.create(Animal.prototype); // 继承父类原型
Dog.prototype.constructor = Dog; // 修正构造函数指向

Dog.prototype.bark = function() {
console.log(this.name + ' says woof!');
};

var myDog = new Dog('Rex', 'German Shepherd');
myDog.eat(); // Rex is eating.
myDog.bark(); // Rex says woof!

总结

  • 原型链继承:通过原型实现继承,缺点是共享引用属性,无法传递构造函数参数。
  • 借用构造函数:通过在子类构造函数中调用父类构造函数实现继承,缺点是无法继承父类的原型方法。
  • 组合继承:结合原型链继承和借用构造函数的优点,解决了引用类型共享的问题,但调用父类构造函数两次。
  • 原型式继承和寄生式继承:通过 Object.create 实现,适合对象的简单继承。
  • 寄生组合式继承:是一种改进的组合继承方式,避免了两次调用父类构造函数的问题,性能最佳。

一般来说,寄生组合继承 是 JavaScript 中最推荐的继承方式。

实现一个 Promise.race

1
2
3
4
5
6
7
8
9
10
function promiseRace(promises){
return new Promise((reslove,rejected){
for(let promise of promises){
Promise.reslove(promise)
.then(reslove)
.then(rejected);
}
})

}

组合继承

组合继承结合了原型链继承和借用构造函数的优点。通过原型链实现对方法的继承,通过借用构造函数实现对属性的继承。

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
// 父类构造函数
function Animal(name) {
this.name = name;
this.canWalk = true;
}

Animal.prototype.eat = function() {
console.log(this.name + ' is eating.');
};

// 子类构造函数
function Dog(name, breed) {
Animal.call(this, name); // 借用构造函数
this.breed = breed;
}

// 原型链继承父类方法
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog; // 修正构造函数指向

Dog.prototype.bark = function() {
console.log(this.name + ' says woof!');
};

var myDog = new Dog('Rex', 'German Shepherd');
myDog.eat(); // Rex is eating.
myDog.bark(); // Rex says woof!

手写深拷贝

判断数据类型和特殊的数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function deepClone(obj) {
// 处理 null 和 非对象类型的情况
if (obj === null || typeof obj !== 'object') return obj;

// 处理特殊类型
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Date) return new Date(obj);

// 初始化结果对象或数组
const res = Array.isArray(obj) ? [] : {};

// 遍历对象的属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 如果是对象,则递归调用深拷贝,否则直接复制
res[key] = deepClone(obj[key]);
}
}

return res;
}

循环引用

使用哈希表

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
function deepClone(obj, hash = new WeakMap()) {
// 处理 null 和 非对象类型的情况
if (obj === null || typeof obj !== 'object') return obj;

// 处理特殊类型
if (obj instanceof RegExp) return new RegExp(obj);
if (obj instanceof Date) return new Date(obj);

// 如果对象已经被拷贝过,直接返回拷贝的结果,避免循环引用
if (hash.has(obj)) return hash.get(obj);

// 初始化结果对象或数组
const res = Array.isArray(obj) ? [] : {};

// 将当前对象存入哈希表中,值为res,避免后续循环引用
hash.set(obj, res);

// 遍历对象的属性
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 递归调用深拷贝函数,传递哈希表用于记录已拷贝的对象
res[key] = deepClone(obj[key], hash);
}
}

return res;
}
  1. WeakMap 的使用
    • WeakMap 用来存储已经拷贝过的对象。WeakMap 的好处是它允许存储的对象键值被垃圾回收,因此非常适合处理对象引用,避免内存泄漏。
    • hash.set(obj, res):当我们开始拷贝某个对象时,我们将其作为键,拷贝的对象副本作为值存入WeakMap中。
    • hash.get(obj):每次遇到对象时,我们首先在WeakMap中检查是否已经拷贝过该对象。如果已经拷贝过,直接返回之前的副本,从而避免循环引用。
  2. 递归过程
    • 函数首先检查输入是否为null或非对象类型,这些类型可以直接返回。
    • 如果对象是特殊类型(如DateRegExp),我们创建它们的副本。
    • 对于普通对象或数组,递归拷贝每个属性,并将已经拷贝的对象存入WeakMap,以防后续出现循环引用。

节流与防抖

闭包

要实现一个 add 方法,使其可以链式调用并累加参数,我们可以利用闭包和函数重载的技巧。具体来说,我们需要一个能够接受任意数量参数的函数,并且返回一个新的函数,这个新的函数可以继续接受参数,直到我们调用它来获取最终的计算结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function add(...args) {
// 内部函数,用于累加参数
const sum = (...innerArgs) => {
// 将外部函数的参数和内部函数的参数合并
args = args.concat(innerArgs);
// 返回一个新的函数,继续接受参数
return sum;
};

// 重写 toString 方法,当函数被转换为字符串时,计算并返回结果
sum.toString = () => {
return args.reduce((acc, curr) => acc + curr, 0);
};

return sum;
}

// 测试用例
console.log(add(1)(2)(3)); // 输出: 6
console.log(add(1, 2, 3)(4)); // 输出: 10
console.log(add(1)(2)(3)(4)(5)); // 输出: 15

解释

  1. add 函数:接受任意数量的初始参数,并返回一个内部函数 sum
  2. sum 函数:接受任意数量的参数,将这些参数与之前的参数合并,并返回自身以继续链式调用。
  3. sum.toString 方法:重写 toString 方法,使得当 sum 函数被转换为字符串时,计算并返回累加结果。这里的 toString 方法会在 console.log 或隐式类型转换(如字符串拼接)时被调用。

关键点

  • 闭包:通过闭包保存所有传递的参数。
  • 链式调用:每次调用 sum 都返回自身,使得可以继续链式调用。
  • 重写 toString 方法:在最终输出时计算累加结果。

使用说明

  • 调用 add 函数时,可以传递任意数量的参数。
  • 每次调用返回一个新的函数,可以继续传递参数。
  • 最终在输出结果时,会调用重写的 toString 方法,计算并返回累加结果。

这样,我们就实现了一个可以链式调用并累加参数的 add 方法。

防抖

防抖的核心思想是:在事件频繁触发时,只执行最后一次触发后的操作。比如用户输入搜索关键词时,我们希望只有用户停止输入一段时间后才执行搜索请求,而不是在每次输入时都执行请求。

1
2
3
4
5
6
7
8
9
10
function debounce(fun, delay) {
let timer = null;
return function (...args) {
let context = this;
clearTimeout(timer);
timer = setTimeout(() => {
fun.apply(context, args);
}, delay)
}
}

节流

节流的核心思想是:在一定时间内,保证事件处理函数只执行一次。比如用户在滚动页面时,我们希望滚动事件的处理函数不要在每次滚动时都执行,而是每隔一段时间执行一次。

使用时间戳实现

使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过 wait 秒之后才执行一次,并且最后一次触发事件不会被执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const throttle = (func, wait) => {
// 上一次执行该函数的时间
let lastTime = 0
return function(...args) {
// 当前时间
let now = new Date()
// 将当前时间和上一次执行函数时间对比
// 如果差值大于设置的等待时间就执行函数
if (now - lastTime > wait) {
lastTime = now
func.apply(this, args)
}
}
}

setInterval(
throttle(() => {
console.log(5555)
}, 1000),
1
)

使用setTimeout

使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function throttle(func, delay) {
let lastTime = 0;

return function(...args) {
const now = Date.now();
const context = this;

// 判断当前时间与上一次执行的时间差是否大于设定的 delay
if (now - lastTime >= delay) {
lastTime = now;
func.apply(context, args);
}
};
}

适用场景:

  • 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动。DOM 元素的拖拽功能实现(mousemove
  • 缩放场景:监控浏览器resize
  • 滚动场景:监听滚动scroll事件判断是否到页面底部自动加载更多
  • 动画场景:避免短时间内多次触发动画引起性能问题

总结

  • 函数防抖

    1
    限制执行次数,多次密集的触发只执行一次

    将几次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

  • 函数节流

    1
    限制执行的频率,按照一定的时间间隔有节奏的执行

    使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。

this创建对象的理解

在 JavaScript 中,使用 new 运算符创建对象实例的过程涉及几个步骤。new 运算符通常与构造函数一起使用,用来创建对象,并确保该对象拥有正确的原型链和属性。

new 运算符的工作过程

当你使用 new 关键字调用一个构造函数时,实际上发生了以下几个步骤:

1. 创建一个新的空对象

首先,new 运算符会创建一个新的空对象,并将这个新对象的 __proto__ 属性链接到构造函数的原型对象。也就是说,新对象会继承构造函数的原型属性。

1
2
const obj = {};
obj.__proto__ = Constructor.prototype;

这一步确保新对象可以访问构造函数的原型属性和方法。

2. 将构造函数的 this 绑定到这个新对象

然后,new 运算符会调用构造函数,并将构造函数内部的 this 绑定到刚创建的对象上。也就是说,构造函数内部的 this 指向这个新创建的对象,从而允许你在构造函数中向新对象添加属性和方法。

1
const result = Constructor.call(obj, ...args);

3. 执行构造函数的代码

构造函数中的代码执行,给新对象添加属性和方法。构造函数中的 this 指向新创建的对象,因此任何通过 this 添加的属性或方法都会被添加到新对象上。

1
2
3
function Constructor(name) {
this.name = name;
}

4. 返回新对象(除非构造函数显式返回对象)

通常情况下,构造函数不需要显式地返回值。new 运算符会自动返回创建的对象。但如果构造函数显式返回一个对象,new 运算符会返回该对象,而不是默认的空对象。

1
2
3
4
if (typeof result === 'object' && result !== null) {
return result;
}
return obj;

new 操作符的完整过程(伪代码)

结合上面的步骤,new 运算符的大致流程可以表示为以下伪代码:

1
2
3
4
5
6
7
8
9
10
function myNew(Constructor, ...args) {
// 1. 创建一个新的空对象,并设置原型链
const obj = Object.create(Constructor.prototype);

// 2. 执行构造函数,将 `this` 绑定到新创建的对象上
const result = Constructor.apply(obj, args);

// 3. 返回新创建的对象,除非构造函数返回一个对象
return typeof result === 'object' && result !== null ? result : obj;
}

示例

1
2
3
4
5
6
7
8
9
10
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
}

const person1 = new Person('Alice', 25);
person1.sayHello(); // 输出: Hello, my name is Alice
  1. 创建空对象new 运算符会创建一个空对象,假设为 person1
  2. 设置原型链person1.__proto__ 会被设置为 Person.prototype,因此它可以访问 Person 构造函数的原型属性和方法。
  3. **绑定 this**:构造函数 Person 中的 this 会被绑定到新创建的 person1 对象。
  4. 执行构造函数this.name = name;this.age = age; 会在 person1 对象上创建 nameage 属性。
  5. 返回新对象person1 对象最终作为结果被返回。

总结

  • new 关键字的主要作用是创建一个新的对象实例,并确保该实例具有正确的原型链。
  • 它会创建一个新的空对象,将构造函数的 this 绑定到这个对象,并执行构造函数的代码。
  • 最后,new 会返回创建的对象,除非构造函数显式返回了一个对象。

new 是 JavaScript 中创建对象的重要方式,它不仅可以确保对象继承原型链上的属性和方法,还允许构造函数为新对象添加实例属性。

理解 __proto__prototype

原型链的终点是null,表示没有更高层次的模型。

示例代码

1
2
3
4
5
6
7
8
9
function Person(name) {
this.name = name;
}

Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};

const person1 = new Person('Alice');

图解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
           Person (构造函数)
+----------------------------+
| function Person(name) {...} |
+----------------------------+
|
v
+-----------------------------------------+
| Person.prototype | <-- 每个由 Person 创建的对象的原型
| +-----------------------------------+ |
| | sayHello: function() {...} | | <-- 共享的原型方法
| +-----------------------------------+ |
+-----------------------------------------+
^
|
|
+---------------------------------------------------+
| person1 | <-- person1 对象实例
| +---------------------------------------------+ |
| | name: "Alice" | | <-- 实例属性
| +---------------------------------------------+ |
| __proto__ | <-- __proto__ 指向构造函数的 prototype
+---------------------------------------------------+

解释:

  1. Person 构造函数

    • Person 是一个构造函数,用来创建对象实例。通过 new Person() 可以创建新的对象。
  2. **Person.prototype**:

    • Person.prototype 是一个对象,存储了所有由 Person 创建的实例对象共享的方法和属性。在这个例子中,Person.prototype 包含一个方法 sayHello
    • 注意:每个构造函数都有一个默认的 prototype 属性。
  3. person1 实例

    • person1 是通过 new Person('Alice') 创建的对象实例。它有一个名为 name 的实例属性,值为 "Alice"
    • person1__proto__ 属性指向 Person.prototype,这意味着它可以访问 Person.prototype 上定义的共享方法 sayHello
  4. **__proto__**:

    • person1.__proto__ 指向构造函数 Personprototype 对象。因此,person1 可以访问 Person.prototype 上的所有属性和方法(例如 sayHello 方法)。

原型链

通过 person1.__proto__ 指向 Person.prototype,以及 Person.prototype 指向 Object.prototype,形成了一个原型链:

1
person1.__proto__ (指向) --> Person.prototype (指向) --> Object.prototype (指向) --> null
  • **person1.__proto__**:指向 Person.prototype,它是 person1 的直接原型。
  • **Person.prototype**:最终继承自 Object.prototype,这是所有对象的原型。
  • **Object.prototype**:原型链的最顶端,所有对象的祖先原型。

完整的原型链图解

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
+--------------------------------------------------+
| person1 |
| +--------------------------------------------+ |
| | name: "Alice" | | <-- 实例属性
| +--------------------------------------------+ |
| | __proto__ | | <-- 指向 Person.prototype
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| Person.prototype |
| +--------------------------------------------+ |
| | sayHello: function() {...} | | <-- 共享方法
| +--------------------------------------------+ |
| | __proto__ | | <-- 指向 Object.prototype
+--------------------------------------------------+
|
v
+--------------------------------------------------+
| Object.prototype |
| +--------------------------------------------+ |
| | toString: function() {...} | | <-- 所有对象共享的方法
| +--------------------------------------------+ |
| | __proto__ | | <-- 指向 null
+--------------------------------------------------+
|
v
null
  • **Person.prototype.__proto__**:指向 Object.prototype,因为 Person.prototype 也是一个普通对象,它从 Object 中继承了通用方法,如 toString()
  • **Object.prototype.__proto__**:指向 null,表示原型链的终点。

通过这种方式,我们可以清晰地看到对象与其原型以及构造函数之间的关系,__proto__prototype 是如何在 JavaScript 的原型链中工作的。

柯里化