参考:[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(); myDog.bark();
|
构造函数
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(); dog.bark();
|
寄生组合式继承(最佳方式)
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(); myDog.bark();
|
总结
- 原型链继承:通过原型实现继承,缺点是共享引用属性,无法传递构造函数参数。
- 借用构造函数:通过在子类构造函数中调用父类构造函数实现继承,缺点是无法继承父类的原型方法。
- 组合继承:结合原型链继承和借用构造函数的优点,解决了引用类型共享的问题,但调用父类构造函数两次。
- 原型式继承和寄生式继承:通过 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(); myDog.bark();
|
手写深拷贝
判断数据类型和特殊的数据类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function deepClone(obj) { 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()) { 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) ? [] : {};
hash.set(obj, res);
for (let key in obj) { if (obj.hasOwnProperty(key)) { res[key] = deepClone(obj[key], hash); } }
return res; }
|
- WeakMap 的使用:
WeakMap 用来存储已经拷贝过的对象。WeakMap 的好处是它允许存储的对象键值被垃圾回收,因此非常适合处理对象引用,避免内存泄漏。
hash.set(obj, res):当我们开始拷贝某个对象时,我们将其作为键,拷贝的对象副本作为值存入WeakMap中。
hash.get(obj):每次遇到对象时,我们首先在WeakMap中检查是否已经拷贝过该对象。如果已经拷贝过,直接返回之前的副本,从而避免循环引用。
- 递归过程:
- 函数首先检查输入是否为
null或非对象类型,这些类型可以直接返回。
- 如果对象是特殊类型(如
Date或RegExp),我们创建它们的副本。
- 对于普通对象或数组,递归拷贝每个属性,并将已经拷贝的对象存入
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; };
sum.toString = () => { return args.reduce((acc, curr) => acc + curr, 0); };
return sum; }
console.log(add(1)(2)(3)); console.log(add(1, 2, 3)(4)); console.log(add(1)(2)(3)(4)(5));
|
解释
add 函数:接受任意数量的初始参数,并返回一个内部函数 sum。
sum 函数:接受任意数量的参数,将这些参数与之前的参数合并,并返回自身以继续链式调用。
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;
if (now - lastTime >= delay) { lastTime = now; func.apply(context, args); } }; }
|
适用场景:
- 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动。
DOM 元素的拖拽功能实现(mousemove)
- 缩放场景:监控浏览器
resize
- 滚动场景:监听滚动
scroll事件判断是否到页面底部自动加载更多
- 动画场景:避免短时间内多次触发动画引起性能问题
总结
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) { const obj = Object.create(Constructor.prototype);
const result = Constructor.apply(obj, args);
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();
|
- 创建空对象:
new 运算符会创建一个空对象,假设为 person1。
- 设置原型链:
person1.__proto__ 会被设置为 Person.prototype,因此它可以访问 Person 构造函数的原型属性和方法。
- **绑定
this**:构造函数 Person 中的 this 会被绑定到新创建的 person1 对象。
- 执行构造函数:
this.name = name; 和 this.age = age; 会在 person1 对象上创建 name 和 age 属性。
- 返回新对象:
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 +---------------------------------------------------+
|
解释:
Person 构造函数:
Person 是一个构造函数,用来创建对象实例。通过 new Person() 可以创建新的对象。
**Person.prototype**:
Person.prototype 是一个对象,存储了所有由 Person 创建的实例对象共享的方法和属性。在这个例子中,Person.prototype 包含一个方法 sayHello。
- 注意:每个构造函数都有一个默认的
prototype 属性。
person1 实例:
person1 是通过 new Person('Alice') 创建的对象实例。它有一个名为 name 的实例属性,值为 "Alice"。
person1 的 __proto__ 属性指向 Person.prototype,这意味着它可以访问 Person.prototype 上定义的共享方法 sayHello。
**__proto__**:
person1.__proto__ 指向构造函数 Person 的 prototype 对象。因此,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 的原型链中工作的。
柯里化