Javascript手写代码
参考:[https://juejin.cn/post/6844903911686406158]
继承
原型链继承
子类的原型被设置为父类的实例,从而使子类能够访问父类的属性和方法。
1 | // 父类构造函数 |
构造函数
1 | function Animal(name){ |
寄生继承
寄生式继承是在原型式继承的基础上进行增强,创建一个新的对象并增强它的功能。
1 | function createDog(original) { |
寄生组合式继承(最佳方式)
1 | function Animal(name) { |
总结
- 原型链继承:通过原型实现继承,缺点是共享引用属性,无法传递构造函数参数。
- 借用构造函数:通过在子类构造函数中调用父类构造函数实现继承,缺点是无法继承父类的原型方法。
- 组合继承:结合原型链继承和借用构造函数的优点,解决了引用类型共享的问题,但调用父类构造函数两次。
- 原型式继承和寄生式继承:通过 Object.create 实现,适合对象的简单继承。
- 寄生组合式继承:是一种改进的组合继承方式,避免了两次调用父类构造函数的问题,性能最佳。
一般来说,寄生组合继承 是 JavaScript 中最推荐的继承方式。
实现一个 Promise.race
1 | function promiseRace(promises){ |
组合继承
组合继承结合了原型链继承和借用构造函数的优点。通过原型链实现对方法的继承,通过借用构造函数实现对属性的继承。
1 | // 父类构造函数 |
手写深拷贝
判断数据类型和特殊的数据类型
1 | function deepClone(obj) { |
循环引用
使用哈希表
1 | function deepClone(obj, hash = new WeakMap()) { |
- WeakMap 的使用:
WeakMap
用来存储已经拷贝过的对象。WeakMap
的好处是它允许存储的对象键值被垃圾回收,因此非常适合处理对象引用,避免内存泄漏。hash.set(obj, res)
:当我们开始拷贝某个对象时,我们将其作为键,拷贝的对象副本作为值存入WeakMap
中。hash.get(obj)
:每次遇到对象时,我们首先在WeakMap
中检查是否已经拷贝过该对象。如果已经拷贝过,直接返回之前的副本,从而避免循环引用。
- 递归过程:
- 函数首先检查输入是否为
null
或非对象类型,这些类型可以直接返回。 - 如果对象是特殊类型(如
Date
或RegExp
),我们创建它们的副本。 - 对于普通对象或数组,递归拷贝每个属性,并将已经拷贝的对象存入
WeakMap
,以防后续出现循环引用。
- 函数首先检查输入是否为
节流与防抖
闭包
要实现一个 add
方法,使其可以链式调用并累加参数,我们可以利用闭包和函数重载的技巧。具体来说,我们需要一个能够接受任意数量参数的函数,并且返回一个新的函数,这个新的函数可以继续接受参数,直到我们调用它来获取最终的计算结果。
1 | function add(...args) { |
解释
add
函数:接受任意数量的初始参数,并返回一个内部函数sum
。sum
函数:接受任意数量的参数,将这些参数与之前的参数合并,并返回自身以继续链式调用。sum.toString
方法:重写toString
方法,使得当sum
函数被转换为字符串时,计算并返回累加结果。这里的toString
方法会在console.log
或隐式类型转换(如字符串拼接)时被调用。
关键点
- 闭包:通过闭包保存所有传递的参数。
- 链式调用:每次调用
sum
都返回自身,使得可以继续链式调用。 - 重写
toString
方法:在最终输出时计算累加结果。
使用说明
- 调用
add
函数时,可以传递任意数量的参数。 - 每次调用返回一个新的函数,可以继续传递参数。
- 最终在输出结果时,会调用重写的
toString
方法,计算并返回累加结果。
这样,我们就实现了一个可以链式调用并累加参数的 add
方法。
防抖
防抖的核心思想是:在事件频繁触发时,只执行最后一次触发后的操作。比如用户输入搜索关键词时,我们希望只有用户停止输入一段时间后才执行搜索请求,而不是在每次输入时都执行请求。
1 | function debounce(fun, delay) { |
节流
节流的核心思想是:在一定时间内,保证事件处理函数只执行一次。比如用户在滚动页面时,我们希望滚动事件的处理函数不要在每次滚动时都执行,而是每隔一段时间执行一次。
使用时间戳实现
使用时间戳的节流函数会在第一次触发事件时立即执行,以后每过
wait
秒之后才执行一次,并且最后一次触发事件不会被执行。
1 | const throttle = (func, wait) => { |
使用setTimeout
使用定时器的节流函数在第一次触发时不会执行,而是在 delay 秒之后才执行,当最后一次停止触发后,还会再执行一次函数。
1 | function throttle(func, delay) { |
适用场景:
- 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动。
DOM
元素的拖拽功能实现(mousemove
) - 缩放场景:监控浏览器
resize
- 滚动场景:监听滚动
scroll
事件判断是否到页面底部自动加载更多 - 动画场景:避免短时间内多次触发动画引起性能问题
总结
函数防抖
1
限制执行次数,多次密集的触发只执行一次
将几次操作合并为一次操作进行。原理是维护一个计时器,规定在
delay
时间后触发函数,但是在delay
时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。函数节流
1
限制执行的频率,按照一定的时间间隔有节奏的执行
使得一定时间内只触发一次函数。原理是通过判断是否到达一定时间来触发函数。
this创建对象的理解
在 JavaScript 中,使用
new
运算符创建对象实例的过程涉及几个步骤。new
运算符通常与构造函数一起使用,用来创建对象,并确保该对象拥有正确的原型链和属性。
new
运算符的工作过程
当你使用 new
关键字调用一个构造函数时,实际上发生了以下几个步骤:
1. 创建一个新的空对象
首先,new
运算符会创建一个新的空对象,并将这个新对象的 __proto__
属性链接到构造函数的原型对象。也就是说,新对象会继承构造函数的原型属性。
1 | const obj = {}; |
这一步确保新对象可以访问构造函数的原型属性和方法。
2. 将构造函数的 this
绑定到这个新对象
然后,new
运算符会调用构造函数,并将构造函数内部的 this
绑定到刚创建的对象上。也就是说,构造函数内部的 this
指向这个新创建的对象,从而允许你在构造函数中向新对象添加属性和方法。
1 | const result = Constructor.call(obj, ...args); |
3. 执行构造函数的代码
构造函数中的代码执行,给新对象添加属性和方法。构造函数中的 this
指向新创建的对象,因此任何通过 this
添加的属性或方法都会被添加到新对象上。
1 | function Constructor(name) { |
4. 返回新对象(除非构造函数显式返回对象)
通常情况下,构造函数不需要显式地返回值。new
运算符会自动返回创建的对象。但如果构造函数显式返回一个对象,new
运算符会返回该对象,而不是默认的空对象。
1 | if (typeof result === 'object' && result !== null) { |
new
操作符的完整过程(伪代码)
结合上面的步骤,new
运算符的大致流程可以表示为以下伪代码:
1 | function myNew(Constructor, ...args) { |
示例
1 | function Person(name, age) { |
- 创建空对象:
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 | function Person(name) { |
图解
1 | Person (构造函数) |
解释:
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 | +--------------------------------------------------+ |
- **
Person.prototype.__proto__
**:指向Object.prototype
,因为Person.prototype
也是一个普通对象,它从Object
中继承了通用方法,如toString()
。 - **
Object.prototype.__proto__
**:指向null
,表示原型链的终点。
通过这种方式,我们可以清晰地看到对象与其原型以及构造函数之间的关系,__proto__
和 prototype
是如何在 JavaScript 的原型链中工作的。