类型判断
主要是利用 Object.prototype.toString.call()
,其中toString
方法返回反映这个对象的字符串。
如果此方法在自定义对象中未被覆盖,
toString()
返回 “[object type]”,其中type
是对象的类型。以下代码说明了这一点:var o = new Object(); o.toString(); // returns [object Object]
具体查看:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/toString
不直接使用 obj.toString()
的原因:
在js中所有的对象类型(Array,Date,Function等)都是Object的实例,Object有一个toString方法也被这些实例继承下来,但是这些实例都无一例外的重写了toString方法,根据原型链的知识,直接调用toString,调用的只是重写过后的toString方法,所以也就不会像Object那样返回带有类型的字符串了。
自定义方法:
function typeOf(obj) {
return Object.prototype.toString.call(obj).slice(8,-1).toLowerCase()
}
new关键字执行的五个步骤,实现new
function mynew(Fn, ...args) {
// 1. 创建一个新对象
const obj = {}
// 2. 修改新对象原型指向为构造函数原型对象
Object.setPrototypeOf(obj, Fn.prototype)
// obj.__proto__ = Fn.prototype
// 3. 将this指向为新对象
// 4. 执行构造函数内部代码
const res = Fn.apply(obj, args) // 同时接收执行的结果
// 5.根据返回值判断
return res instanceof Object ? res : obj
}
数组扁平化
let arr = [1,2,[3,4],[5,6,[7,8,9]]]
console.log(arr.flat(Infinity))
console.log(arr.toString().split(',').map(Number))
console.log(arr.join().split(',').map(Number))
简单实现flat()
// 利用递归
Array.prototype.flatten = function () {
let res = [], len = this.length
for (let i = 0; i < len; i++) {
if (Array.isArray(this[i])) {
res = res.concat(this[i].flatten())
} else res.push(this[i])
}
return res
}
// reduce + concat 无限递归
const flatten = (arr) => {
return arr.reduce(
(acc, cur) => (Array.isArray(cur) ? acc.concat(flatten(cur)) : acc.concat(cur)),
[]
)
}
深拷贝
简单的实现方式:
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj))
}
局限很大,没法clone函数、symbol等。
call、apply、bind
手写call
Function.prototype.mycall = function(context) {
// 获取上下文ctx,同时确保传入的上下文参数为null或undefined时替换为指向全局对象
const ctx = context || globalThis // 浏览器环境下全局对象为window
// 给这个上下文添加一个函数本身的属性方法
ctx.fn = this
// 获取参数
const args = [...arguments].slice(1)
// 调用函数,因为是ctx.fn()的形式,这个fn的this自然指向ctx
const res = ctx.fn(...args)
// 删除这个临时使用的fn属性方法,因为是同一个内存地址,防止污染这个传入的对象
delete ctx.fn
// 返回结果
return res
}
foreach、map、filter
手写foreach()
因为是第一个,所以再说下foreach
的几个特性:
myMap.forEach(callback[, thisArg])
forEach
方法将对Map
中真实存在的每一个元素执行一次参数中提供的回调函数,它不会对任何已经被删除的元素执行调用。然而,它还会对键存在而值为undefined
的元素执行调用。
callback
函数有三个参数:value
- 元素的值key
- 元素的键Map
- 当前正在被遍历的对象
-
如果参数 forEach 带有一个
thisArg
参数,在调用的时候,这个参数将传给callback
函数作为其 this 的值。否则,函数将默认使用undefined
传给callback
函数作为其this
值。 -
forEach
仅仅是对 Map 对象中的每一个元素执行一遍callback
函数,然后直接返回 undefined。
不多说,直接上代码:
Array.prototype.myforEach = function (fn, context) {
// 确保第一个参数为函数
if (typeof fn !== 'function')
throw new Error(fn + ' is not a function!')
// 第二参数,指定this,如果没有则默认将undefined当做this
const ctx = context || undefined
// 这里的this还是指向调用该方法的数组对象本身
for (let i = 0; i < this.length; i++) {
if (!(i in this)) continue
// 注意这里判断元素不存在的写法,是不会跳过值为undefined和NaN的元素的
fn.call(ctx, this[i], i, this) // 用call指定this
}
}
做个测试:
let arr = [1, , 3, 4, undefined, NaN]
arr.forEach((item, index, val) => {
console.log(item, index, val[index])
})
console.log('---------------')
arr.myforEach((item, index, val) => {
console.log(item, index, val[index])
})
/*
1 0 1
3 2 3
4 3 4
undefined 4 undefined
NaN 5 NaN
--------------
1 0 1
3 2 3
4 3 4
undefined 4 undefined
NaN 5 NaN
*/
手写map()
和foreach差不多,不过会返回一个执行回调函数后的新数组。这里不多说了。
Map的几个注意点:
- Map生成的是一个新数组
- 有第二参数,为指定的this,如果没有则默认this丢失
- 对于稀疏数组,返回的仍然是稀疏数组(即map会保留“不存在”的元素)
Array.prototype.myMap = function (fn, context) {
if (typeof fn !== 'function')
throw new Error(fn + ' is not a function!')
let _arr = []
const ctx = context || Object.create(null) // 继续用undefined也可以
for (let i = 0; i < this.length; i++) {
// 当调用的数组为稀疏数组时,要确保返回的数组也是稀疏的
// 这里有两种写法:
// 1. concat + push
if (!(i in this)) _arr = _arr.concat([,])
else _arr.push(fn.call(ctx, this[i], i, arr))
/* 2. continue + 索引添加元素
if(!(i in this)) continue
else _arr[i] = fn.call(ctx, this[i], i, arr)
*/
}
return _arr
}
做个测试:
let arr = [1, , 2, 3, undefined, NaN]
console.log( arr.map((val, index) => val * index) ) // [ 0, <1 empty item>, 4, 9, NaN, NaN ]
console.log( arr.myMap((val, index) => val * index) ) // [ 0, <1 empty item>, 4, 9, NaN, NaN ]
手写filter()
filter的注意点:
- filter同样是创建一个新函数
- 有第二参数,为指定的this,如果没有则默认this丢失
- filter会直接跳过不存在的元素
Array.prototype.myFilter = function (fn, context) {
if (typeof fn !== 'function')
throw new Error(fn + ' is not a function!')
let _arr = []
const ctx = context || Object.create(null)
for (let i = 0; i <= this.length; i++) {
if(!this[i]) continue // 如果值为undefined、NaN或者不存在的时候直接跳过去
if(fn.call(ctx, this[i], i, this)) _arr.push(this[i])
}
return _arr
}
做个测试:
let arr = [1, , 2, 3, undefined, NaN]
console.log(arr.filter((item, index) => item > index)) // [ 1, 3, 4 ]
console.log(arr.myFilter((item, index) => item > index)) // [ 1, 3, 4 ]
防抖
function debounce(fn, delay = 500) { // 一个要执行防抖的函数,以及延时参数,这里默认500ms
// 在外部事先定义这个计时器,作为公共定时器变量,确保最后只会执行一个定时器,这样也就只会触发一次事件
let timeout = null
return function () {
clearTimeout(timeout) // 清除上一个定时器
const ctx = this // 保存this
const args = arguments; // 参数列表,拿到event对象
// 定义定时器,最后只会触发一次
timeout = setTimeout(function () {
fn.apply(ctx, args) // 利用apply,确保函数的this指向不变
// 当然这里也可以直接使用箭头函数,这样这里的this就无需提前保存
}, delay)
}
}
节流
一个普通版本的:
function throttle(fn, delay = 500) {
let timeout = null
return function () {
// 如果已经有定时器了,直接返回不执行
if (timeout) return
// 如果没有,那么开始一个定时任务
timeout = setTimeout((...args) => {
//
fn.apply(this, args)
timeout = null
}, delay)
}
}