目录
1. 手写深拷贝
function deepClone(startObj,endObj) {
let obj = endObj || {}
for (let i in startObj) {
if (typeof startObj[i] === 'object') {
startObj[i].constructor === Array ? obj[i] = [] : obj[i] = {}
deepClone(startObj[i],obj[i])
} else {
obj[i] = startObj[i]
}
}
return obj
}
值得注意的一点是,在递归调用的时候,需要把当前处理的 obj[i] 给传回去,否则的话 每次递归obj都会被赋值为空对象,就会对已经克隆好的数据产生影响。
我们验证一下深拷贝是否实现:
const person = {
name: 'zyj',
age: 20,
sister: {
name: 'duoduo',
age: 13,
mother: {
name: 'lili',
age:45
}
}
}
const newPerson = deepClone(person)
newPerson.sister.mother.age = 50
console.log(newPerson)
// {
// name: 'zyj',
// age: 20,
// sister: { name: 'duoduo', age: 13, mother: { name: 'lili', age: 50 } }
// }
console.log(person)
// {
// name: 'zyj',
// age: 20,
// sister: { name: 'duoduo', age: 13, mother: { name: 'lili', age: 45 } }
// }
2. 防抖函数
单位时间内,频繁触发一个事件,以最后一次触发为准。
function debounce(fn,delay) {
let timer = null
return function() {
clearTimeout(timer)
timer = setTimeout(() => {
fn.call(this)
}, delay);
}
}
我们看一下调用流程:
<body>
<input type="text">
<script>
const input = document.querySelector('input')
input.addEventListener('input',debounce(function() {
console.log(111);
},1000))
function debounce(fn,delay) {
let timer = null
return function() {
clearTimeout(timer)
timer = setTimeout(() => {
fn.call(this)
}, delay);
}
}
</script>
</body>
可能有些同学对 fn.call(this) 不太明白,在 debounce 中我们把匿名函数作为参数传进来,因为匿名函数的执行环境具有全局性,所以它的 this 一般指向 window ,所以要改变一下 this 指向,让它指向调用者 input 。
3. 节流函数
单位时间内,频繁触发一个事件,只会触发一次。
function throttle(fn,delay) {
return function () {
if (fn.t) return;//每次触发事件时,如果当前有等待执行的延时函数,则直接return
fn.t = setTimeout(() => {
fn.call(this);//确保执行函数中this指向事件源,而不是window
fn.t = null//执行完后设置 fn.t 为空,这样就能再次开启新的定时器
}, delay);
};
}
调用流程:
<script>
//节流throttle代码:
function throttle(fn,delay) {
return function () {
if (fn.t) return;//每次触发事件时,如果当前有等待执行的延时函数,则直接return
fn.t = setTimeout(() => {
fn.call(this);//确保执行函数中this指向事件源,而不是window
fn.t = null//执行完后设置 fn.t 为空,这样就能再次开启新的定时器
}, delay);
};
}
window.addEventListener('resize', throttle(function() {
console.log(11);
},1000));
</script>
只有当调整浏览器视口大小时才会输出,且每隔一秒输出一次
4. 模拟 instanceof
// 模拟 instanceof
function myInstance(L, R) {
//L 表示左表达式,R 表示右表达式
let RP = R.prototype; // 取 R 的显示原型
let LP = L.__proto__; // 取 L 的隐式原型
while (true) {
if (LP === null) return false;
if (RP === LP)
// 这里重点:当 O 严格等于 L 时,返回 true
return true;
LP = LP.__proto__;
}
}
function person(name) {
this.name = name
}
const zyj = new person('库里')
console.log(myInstance(zyj,person)); // true
5. 全局通用的数据类型判断方法
function getType(obj){
let type = typeof obj;
if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返回
return type;
}
// 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1'); // 注意正则中间有个空格
}
6. 手写 call 函数
Function.prototype.myCall = function (context) {
// 先判断调用myCall是不是一个函数
// 这里的this就是调用myCall的
if (typeof this !== 'function') {
throw new TypeError("Not a Function")
}
// 不传参数默认为window
context = context || window
// 保存this
context.fn = this
// 保存参数
let args = Array.from(arguments).slice(1)
//Array.from 把伪数组对象转为数组,然后调用 slice 方法,去掉第一个参数
// 调用函数
let result = context.fn(...args)
delete context.fn
return result
}
7. 手写 apply 函数
Function.prototype.myApply = function (context) {
// 判断this是不是函数
if (typeof this !== "function") {
throw new TypeError("Not a Function")
}
let result
// 默认是window
context = context || window
// 保存this
context.fn = this
// 是否传参
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
8. bind方法
在实现手写bind方法的过程中,看了许多篇文章,答案给的都很统一,准确,但是不知其所以然,所以我们就好好剖析一下bind方法的实现过程。
我们先看一下bind函数做了什么:
bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。
读到这里我们就发现,他和 apply , call 是不是很像,所以这里指定 this 功能,就可以借助 apply 去实现:
Function.prototype.myBind = function (context) {
// 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
const self = this;
// 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
// 这里产生了闭包
const args = Array.from(arguments).slice(1)
return function () {
// 这个时候的 arguments 是指 myBind 返回的函数传入的参数
const bindArgs = Array.from(arguments)
// 合并
return self.apply(context, args.concat(bindArgs));
};
};
大家对这段代码应该都能看懂,实现原理和手写 call , apply 都很像,因为 bind 可以通过返回的函数传参,所以在 return 里面获取的 bindArgs 就是这个意思,然后最后通过 concat 把原来的参数和后来传进来的参数进行数组合并。
我们来看一下结果:
const person = {
name: 'zyj'
}
function man(age) {
console.log(this.name);
console.log(age)
}
const test = man.myBind(person)
test(18)//zyj 18
现在重点来了,bind 区别于 call 和 apply 的地方在于它可以返回一个函数,然后把这个函数当作构造函数通过 new 操作符来创建对象。
我们来试一下:
const person = {
name: 'zyj'
}
function man(age) {
console.log(this.name);
console.log(age)
}
const test = man.myBind(person)
const newTest = new test(18) // zyj 18
这是用的我们上面写的 myBind 函数是这个结果,那原生 bind 呢?
const person = {
name: 'zyj'
}
function man(age) {
console.log(this.name);
console.log(age)
}
const test = man.bind(person)
const newTest = new test(18) // undefined 18
由上述代码可见,使用原生
bind
生成绑定函数后,通过new
操作符调用该函数时,this.name 是一个 undefined,这其实很好理解,因为我们 new 了一个新的实例,那么构造函数里的 this 肯定指向的就是实例,而我们的代码逻辑中指向的始终都是 context ,也就是传进去的参数。
所以现在我们要加个判断逻辑:
Function.prototype.myBind = function (context) {
// 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
const self = this;
// 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
// 这里产生了闭包
const args = Array.from(arguments).slice(1)
const theBind = function () {
const bindArgs = Array.from(arguments);
// 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例
// 当作为普通函数时,将绑定函数的 this 指向 context 即可
// this instanceof fBound 的 this 就是绑定函数的调用者
return self.apply(
this instanceof theBind ? this : context,
args.concat(bindArgs)
);
};
return theBind;
};
现在这个效果我们也实现了,那我们的 myBind 函数就和其他的原生 bind 一样了吗?来看下面的代码:
const person = {
name: 'zyj'
}
function man(age) {
console.log(this.name);
console.log(age)
}
man.prototype.sayHi = function() {
console.log('hello')
}
const test = man.myBind(person)
const newTest = new test(18) // undefined 18
newTest.sayHi()
如果 newTest 是我们 new 出来的 man 实例,那根据原型链的知识,定义在man的原型对象上的方法肯定会被继承下来,所以我们通过 newTest.sayHi 调用能正常输出 hello 么?
该版代码的改进思路在于,将返回的绑定函数的原型对象的
__proto__
属性,修改为原函数的原型对象。便可满足原有的继承关系。
Function.prototype.myBind = function (context) {
// 这里的 this/self 指的是需要进行绑定的函数本身,比如用例中的 man
const self = this;
// 获取 myBind 函数从第二个参数到最后一个参数(第一个参数是 context)
// 这里产生了闭包
const args = Array.from(arguments).slice(1);
const theBind = function () {
const bindArgs = Array.from(arguments);
// 当绑定函数作为构造函数时,其内部的 this 应该指向实例,此时需要更改绑定函数的 this 为实例
// 当作为普通函数时,将绑定函数的 this 指向 context 即可
// this instanceof fBound 的 this 就是绑定函数的调用者
return self.apply(
this instanceof theBind ? this : context,
args.concat(bindArgs)
);
};
theBind.prototype = Object.create(self.prototype)
return theBind;
};
9. 模拟 new
// 手写一个new
function myNew(fn, ...args) {
// 创建一个空对象
let obj = {}
// 使空对象的隐式原型指向原函数的显式原型
obj.__proto__ = fn.prototype
// this指向obj
let result = fn.apply(obj, args)
// 返回
return result instanceof Object ? result : obj
}
有很多小伙伴不明白为什么要判断 result 是不是 Object 的实例,我们首先得了解,在JavaScript中构造函数可以有返回值也可以没有。
1. 没有返回值的情况返回实例化的对象
function Person(name, age){
this.name = name
this.age = age
}
console.log(Person()); //undefined
console.log(new Person('zyj',20));//Person { name: 'zyj', age: 20 }
2. 如果存在返回值则检查其返回值是否为引用类型,如果为非引用类型,如(string,number,boolean,null,undefined),上述几种类型的情况与没有返回值的情况相同,实际返回实例化的对象
function Person(name, age){
this.name = name
this.age = age
return 'lalala'
}
console.log(Person()); //lalala
console.log(new Person('zyj',20));//Person { name: 'zyj', age: 20 }
3. 如果存在返回值是引用类型,则实际返回该引用类型
function Person(name, age){
this.name = name
this.age = age
return {
name: 'curry',
ahe: 34
}
}
console.log(Person()); //{ name: 'curry', ahe: 34 }
console.log(new Person('zyj',20));//{ name: 'curry', ahe: 34 }
10. 类数组转化为数组的方法
const arrayLike=document.querySelectorAll('div')
// 1.扩展运算符
[...arrayLike]
// 2.Array.from
Array.from(arrayLike)
// 3.Array.prototype.slice
Array.prototype.slice.call(arrayLike)
// 4.Array.apply
Array.apply(null, arrayLike)
// 5.Array.prototype.concat
Array.prototype.concat.apply([], arrayLike)
11. 组合继承
function father (name) {
this.name = name
this.age = 18
}
father.prototype.getName = function(){} // 方法定义在父类原型上(公共区域)
function child () {
// 继承父类属性,可传入参数
father.call(this,'Tom')
// 将会生成如下属性:
// name:'tom'
// age: 18
}
child.prototype = new father() // 重写原型对象
child.prototype.constructor = child
这里的原型链关系应该是这样的:
该方式也叫做伪经典继承。其核心思路是:重写子类的原型对象为父类实例,并通过盗用构造函数继承父类实例的属性。
12. 原型式继承
基本思路是,对传入的对象做了一次浅复制,并赋值给一个空函数
F
(临时类型)的原型对象,并返回一个通过F
生成的实例。这个实例的__proto__
自然而然地指向了传入的对象,可以理解为一个挂钩🧷的过程。
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
let father = function() {}
father.prototype.getName = function() {
console.log('zyj')
}
let son = object(father)
let daughter = object(father)
son.prototype.getName() // zyj
大概是这么个过程:
在 ECMAScript 5
中,通过增加 Object.create()
方法将原型式继承的概念规范化,即替代了上述自定义的 object()
函数。所以对于 Object.create()
的手写实现,核心思路与上述的自定义函数类似,只是添加了部分参数校验的环节。
let son = Object.create(father) // 等同于上述代码
13. 实现 Object.create()
Object.myCreate = function(proto, propertyObject) {
// 参数校验
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object or null.')
// 不能传一个 null 值给实例作为属性
if (propertyObject == null) {
new TypeError('Cannot convert undefined or null to object')
}
// 原型式继承的思想:用一个空函数(即忽略掉原有构造函数的初始化代码)创建一个干净的实例
function F() {}
F.prototype = proto // 确定后续的继承关系
const obj = new F()
// 如果有传入第二个参数,将其设为 obj 的属性
if (propertyObject != undefined) {
Object.defineProperties(obj, propertyObject)
}
// 即 Object.create(null) 创建一个没有原型对象的对象
if (proto === null) {
obj.__proto__ = null
}
return obj
}
14. 数组去重
ES5实现:
function unique(arr) {
var res = arr.filter(function(item, index, array) {
return array.indexOf(item) === index
})
return res
}
ES6实现:
var unique = arr => [...new Set(arr)]