前言
本篇主要归纳自己写过的一些JavaScript代码,它包含了一些实现方式,如去重的方式,数组扁平化的方式,防抖节流、发布订阅等,又或者包含了一些源码,new、call、apply、bind、数组方法等,统一将这些作为一个系列。
一、实现系列
1.数组去重
1.1 set
由于set的特点:所有元素值都是独一无二的,因此能用来去重,也是最简单的方式
function removeRepeat(arr) {
return [...new Set(arr)]
}
1.2 indexOf
通过indexOf查找数据返回第一个索引的方式,可以通过新建数组,无则push有则略过。
function removeRepeat(arr) {
let res = [];
for (let val of arr) {
if (res.indexOf(val) === -1) {
res.push(val)
}
}
return res
}
1.3 indexOf+filter
利用filter的条件过滤,我们返回的是索引号等于indexOf值的元素,也就是只让每个元素出现的第一次加入
function removeRepeat(arr) {
let res = arr.filter(function (val, index) {
return arr.indexOf(val) === index
})
return res
}
1.4 sort+splice
将数组先进行排序,然后从第二个元素开始for循环,如果等于前一个元素,则将当前元素删除。注意删除元素后索引需要减一,因为删除后数组减小,本来下一位的元素的索引变成了当前的索引值。
function removeRepeat(arr) {
arr.sort((a, b) => a - b);
for (let i = 1; i < arr.length; i++) {
if (arr[i] === arr[i - 1]) {
arr.splice(i, 1)
i--;
}
}
return arr
}
1.5 sort
同上种方法相比,这个不需要splice,但是需要创建一个新的数组,当然也可以结合filter
function removeRepeat(arr) {
arr.sort((a, b) => a - b);
let res = [arr[0]];
for (let i = 1; i < arr.length; i++) {
if (arr[i] != arr[i - 1]) {
res.push(arr[i])
}
}
return res
}
1.6 map
也可以借助map的映射关系来判断是否已存在,从而取唯一值
function removeRepeat(arr) {
let map = new Map();
let res = [];
for (let val of arr) {
if (!map.has(val)) {
map.set(val, true)
res.push(val)
}
}
return res
}
1.7 includes
includes是一个可以判断某元素是否存在的数组方法,返回布尔值
function removeRepeat(arr) {
let res = [];
for (let val of arr) {
if (!res.includes(val)) {
res.push(val)
}
}
return res
}
2.数组扁平化
就是将多维数组转为一维数组
2.1 flat
flat方法是Array直接可调用的扁平化方法,传入参数为扁平化的深度,默认为1,infinity为扁平任意深度
function delayering(arr) {
return arr.flat(Infinity)
}
2.2 递归
递归,如果是数组继续调用自身,如果是值直接push
function delayering(arr) {
let res = [];
fn(arr);
function fn(item) {
if (Array.isArray(item)) {
for (let val of item) {
fn(val)
}
} else {
res.push(item)
}
}
return res
}
2.3 扩展符
借用对数组调用一次扩展符运算会扁平一次的特性
Array.prototype.flat = function (deep) {
let arr = this;
let res = [];
for (let item of arr) {
if (Array.isArray(item) && deep > 0) {
res.push(...item.flat(deep - 1));
} else {
res.push(item);
}
}
return res;
};
let arr = [1, 2, 3, [4, 5, [6]]];
console.log(arr.flat(1));
2.4 reduce+concat
利用reduce设置开始数组为空,判断当前数组是否为数组,为数组则再次调用,为值则进行拼接
function delayering(arr) {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? delayering(cur) : cur)
}, [])
}
2.5 正则
这是一种比较取巧的方式,通过正则匹配将所有[]转为空再进行转为数组,不过此时所有元素都是字符串形式,需要根据需求转类型
function delayering(arr) {
let reg = /\[|\]/g;
return arr.toString().replace(reg, '').split(',')
}
3.类数组转为数组
类数组类似数组,它有自己的length,但实际是对象。如arguments、querySelectorAll选择的dom等,这里以arguments为例将类数组转为数组
3.1 Array.form
Array.from()
方法对一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。
function getArr() {
return Array.from(arguments)
}
3.2 扩展符
扩展符还可以将类数组转为数组
function getArr() {
return [...arguments]
}
3.3 Array.prototype.slice.call
通过此方法让类数组对象拥有slice方法进行截取,返回数组
function getArr() {
return Array.prototype.slice.call(arguments)
}
4.清除空格字符串
4.1 trim
trim可以清除字符串两旁的空格,不能清除中间的空格
function clearSpaces(str) {
return str.trim()
}
4.2 正则
正则匹配进行替换
function clearSpaces(str) {
return str.replace(/\s/g, '')
}
4.3 split+join
function clearSpaces(str) {
return str.split(' ').join('')
}
5.判断回文字符串
回文字符串就是从前往后看和从后往前看都是相等的
5.1 reverse
转为数组调用reverse方法再转为字符串进行对比
function isPalindrome(str) {
return str === str.split('').reverse().join('')
}
5.2 双指针
从两旁往中间移如果一直相等到结束,则为回文字符串
function isPalindrome(str) {
let left = 0;
let right = str.length - 1;
while (left < right) {
if (str[left] != str[right]) return false;
left++;
right--;
}
return true;
}
6.字符串转驼峰
6.1 split(’-’)
通过split按’-'分割然后将后续字符第一个大写进行拼接
function translateToHump(str) {
let arr = str.split('-');
for (let i = 1; i < arr.length; i++) {
arr[i] = arr[i][0].toUpperCase() + arr[i].substring(1)
}
return arr.join('')
}
6.2 split+splice
将字符串直接分割成数组,遍历删除’-’,后一位大写,最后转字符串返回
function translateToHump(str) {
let arr = str.split('');
for (let i = 1; i < arr.length; i++) {
if (arr[i] === '-') {
arr.splice(i, 1);
arr[i] = arr[i].toUpperCase()
}
}
return arr.join('')
}
6.3 正则匹配
通过正则匹配-和-后的首字母,然后将首字母大写返回
function translateToHump(str) {
let reg = /\-(\w)/g;
return str.replace(reg, function (_0, _1) {
return _1.toUpperCase()
})
}
7.驼峰转字符串
7.1 split+splice
转为数组,循环判断是否为大写,是则转为小写并且前面添加’-’
function translateToStr(hump) {
let arr = hump.split('');
for (let i = 1; i < arr.length; i++) {
if (arr[i] >= 'A' && arr[i] <= 'Z') {
arr[i] = arr[i].toLowerCase()
arr.splice(i, 0, '-')
}
}
return arr.join('')
}
7.2 正则
正则匹配大写字母,然后加上’-'进行返回
function translateToStr(hump) {
let reg = /([A-Z])/g
return hump.replace(reg, function (_0, _1) {
return '-' + _1.toLowerCase()
})
}
8.闭包相关
8.1 防抖
在一定时间段内,将多次高频操作转为最后一次执行。如果相邻两次操作小于设置的时间,则会清除上次执行,调用当前执行。可设置immediate来满足是否需要第一次立即执行
function debounce(fn, delay, immediate) {
let timer = null;
return function (...args) {
if (immediate && !timer) {
fn.call(this, ...args)
}
timer && clearTimeout(timer)
timer = setTimeout(() => {
fn.call(this, ...args)
}, delay);
}
}
返回函数引用了timer,产生了闭包,作用在于能使高频操作时公用一个timer。
8.2 节流
在一定时间段内,将多次高频操作划分为等间隔段执行的低频操作,通过设置一个节流阀来控制是否可执行。
function throttle(fn, delay) {
let flag = true;
return function (...args) {
if (flag) {
flag = false;
setTimeout(() => {
flag = true;
fn.call(this, ...args)
}, delay);
}
}
}
返回函数引用了flag,产生了闭包,作用在于能使高频操作时公用一个flag。
8.3 单例
单例只允许触发一次,之后再次触发会将之前的直接返回。
这里以创建一个dom元素为例
function createInstance() {
let div = document.createElement('div')
div.innerHTML = 'hello world'
document.body.appendChild(div)
return div
}
//主要代码
function getSingle(fn) {
let instance = null;
return function () {
instance || (instance = fn());
}
}
let funcSingle = getSingle(createInstance)
btn.addEventListener('click', function () {
funcSingle()
})
中间为单例的主要代码,通过闭包使instance存活,后续调用由于已经存在instance也就不会去执行createInstance。
9.深浅拷贝
1.浅拷贝
浅拷贝创建一个新对象,对于基本类型直接拷贝,引用类型拷贝引用地址
function shallowCopy(source) {
if(typeof source!='object'||source===null){
return source
}
cosnt target = {};
for (let i in source) {
target[i] = source[i]
}
return target
}
浅拷贝还有Object.assign,会在后续的源码系列
2.深拷贝
深拷贝创建一个新对象,对于基本类型直接拷贝,引用类型则创建新空间拷贝数据。通过weakmap保存缓存过的数据信息,可以解决对象循环引用的问题
function deepCopy(source, map = new WeakMap()) {
if (map.has(source)) return map.get(source)
const target = Array.isArray(source) ? [] : {}
map.set(source, target)
for (let i in source) {
if (typeof source[i] === 'object' && source[i] != null) {
target[i] = deepCopy(source[i], map)
} else {
target[i] = source[i]
}
}
return target
}
10.发布订阅
通过声明一个Sub类,保存一个公共事件池,声明订阅subscibe、取消订阅unsubscribe、发布publish、一次订阅once方法
class Sub {
constructor() {
this.subsribers = {}
}
subsribe(type, fn) {
if (!this.subsribers[type]) {
this.subsribers[type] = []
}
this.subsribers[type].push(fn)
}
unsubsribe(type, fn) {
if (!this.subsribers[type]) return;
this.subsribers[type] = this.subsribers[type].filter(function (item) {
return item !== fn
})
}
publish(type, ...args) {
if (!this.subsribers[type]) return;
this.subsribers[type].forEach(function (fn) {
fn.call(this, ...args)
})
}
once(type, fn) {
let _this = this
function func() {
fn()
_this.unsubsribe(type, func)
}
this.subsribe(type, func)
}
}
2.源码系列
1.数组方法
关于数组的高阶方法,传入两个参数,第一个为函数回调,内部包含了数组单个值item、索引index、数组本身array,第二个参数为this指向
1.1 forEach
用于对数组每一个元素进行一次调用,没有返回值
Array.prototype.myForEach = function (callback, thisArg) {
if (typeof callback != 'function') {
throw new TypeError('sorry,callback is not a function')
}
let len = this.length;
let k = 0;
while (k < len) {
callback.call(thisArg, this[k], k, this)
k++;
}
}
1.2 filter
用于过滤数组,返回符合条件的元素数组,返回值为一个新数组
Array.prototype.myFilter = function (callback, thisArg) {
if (typeof callback != 'function') {
throw new TypeError('Sorry,callback is not a function')
}
let resArr = [];
for (let k = 0; k < this.length; k++) {
if (callback.call(thisArg, this[k], k, this)) {
resArr.push(this[k])
}
}
return resArr
}
1.3 map
用于处理数组的各项返回,返回一个新数组
Array.prototype.myMap = function (callback, thisArg) {
if (typeof callback != 'function') {
throw new TypeError('Sorry,callback is not a function')
}
let resArr = [];
for (let k = 0; k < this.length; k++) {
resArr[k] = callback.call(thisArg, this[k], k, this)
}
return resArr
}
1.4 some
用于判断数组是否存在符合条件的元素,存在则返回true,否则返回false,不改变原数组
Array.prototype.mySome = function (callback, thisArg) {
if (typeof callback != 'function') {
throw new TypeError('Sorry,callback is not a function')
}
for (let k = 0; k < this.length; k++) {
if (callback.call(thisArg, this[k], k, this)) {
return true
}
}
return false
}
1.5 every
用于判断数组所有元素是否满足条件,都满足返回true,一项不满足返回false
Array.prototype.myEvery = function (callback, thisArg) {
if (typeof callback != 'function') {
throw new TypeError('Sorry,callback is not a function')
}
for (let k = 0; k < this.length; k++) {
if (!callback.call(thisArg, this[k], k, this)) {
return false;
}
}
return true;
}
1.6 reduce
传入两个参数,第一个为回调函数,包含了当前元素的上一个值和当前元素值,第二个参数为默认上一个元素值。如果第二个参数没有设置则令第一个元素为上一个值,并改变循环的起始位置
Array.prototype.myReduce = function (callback, initValue) {
if (typeof callback != 'function') {
throw new TypeError('Sorry,callback is not a function')
}
let start = 0;
let preValue = initValue
if (preValue === undefined) {
preValue = this[0];
start = 1;
}
for (let i = start; i < this.length; i++) {
preValue = callback(preValue, this[i])
}
return preValue
}
2.this指向相关
2.1 call
call方法第一个参数为this指向的对象,后续参数为列表形式,立即执行返回结果。
Function.prototype.myCall = function (context, ...args) {
if (context === null || context === undefined) {
context = window;
} else {
context = Object(context)
}
let fn = Symbol()
context[fn] = this;
let res = context[fn](...args);
delete context[fn];
return res
}
过程:
- 判断如果第一个参数为undefined或null,则使它默认为window
- symbol创建唯一属性fn,其等于当前函数,并加入目标对象
- 函数在目标对象中执行获得结果
- 删除函数返回结果
2.2 apply
apply方法和call类似,区别就是第二个参数为数组形式
Function.prototype.myApply = function (context, args) {
if (context === null || context === undefined) {
context = window;
} else {
context = Object(context)
}
let fn = Symbol();
context[fn] = this;
let res = context[fn](args);
delete context[fn];
return res;
}
实现和call类似
2.3 bind
bind与call、apply不同的是它会返回一个新函数,并不会立即执行。它的第二个参数也是参数列表的形式。
Function.prototype.myBind = function (context, ...args) {
let _this = this;
function fn() {}
let res = function (...rest) {
return _this.apply(this instanceof res ? this : context, [...args, ...rest])
}
fn.prototype = this.prototype
res.prototype = new fn()
return res
}
注意:
- 由于new的优先级高于bind,所以需要判断当前的返回函数是不是作为构造函数在使用,如果是的话它的this不变指向原来的实例,如果不是则为传入的第一个参数
- 当前调用bind的函数有些属性方法是存在原型上的,所以需要把原型也赋给返回的函数,但是为了不造成两个原型之间直接性的影响,引入中间函数fn。
3.原型、原型链相关
3.1 new
通过new可以创建一个构造函数的实例,其内部过程如下
- 创建一个新对象
- 使新对象的__proto__指向构造函数的prototype
- 将构造函数的this指向新对象并运行
- 判断3运行的结果是否是对象,是对象代表了构造函数有返回的对象,那就取这个对象,如果不是,则取新对象。
function myNew(fn, ...args) {
let newObj = {}
newObj.__proto__ = fn.prototype;
let res = fn.call(newObj, ...args)
return (res && typeof res === 'object') ? res : newObj
}
3.2 instanceof
通过instanceof可以用来判断一个构造函数的原型在不在一个实例对象的原型链上,本质就是通过原型链的查找,查找到则返回true,查找到原型链顶端null了则说明未找到返回false
function myInstanceOf(left, right) {
if (typeof left != 'object' || left == null) return false
while (true) {
if (left === null) return false;
if (left === right.prototype) return true;
left = left.__proto__
}
}
3.3 Object.create
可用于创造一个新对象,这个新对象的原型prototype是传入的prototype
const myCreate = function (prototype) {
if (typeof prototype != 'object') {
throw new TypeError('Object prototype may only be an Object or null')
}
if (prototype === null) return {}
function fn() {}
fn.prototype = prototype;
return new fn()
}
4.其他
4.1 Object.assign
Object.assign用于对象合并,有两个或多个参数,target、source。第一个target为合并目标对象,后续source为合并到target的对象。如果target为空对象,则可以用来做拷贝,它是浅拷贝。
- 同名属性后续会覆盖
- 可传入多个source对象
- 如果传入不是对象会转为对象
function myAssign() {
let args = Array.prototype.slice.call(arguments);
if (args.length === 0) throw new TypeError('Cannot convert undefined or null to object')
let target = args[0]
if (args.length === 1) return target
let sourceArr = args.slice(1)
for (let i = 0; i < sourceArr.length; i++) {
for (let key in sourceArr[i]) {
if (sourceArr[i].hasOwnProperty(key)) {
target[key] = sourceArr[i][key]
}
}
}
return target
}
4.2 Object.is
Object.i是建立在===号之上,完善了一些特殊情况用来对比是否全等的方法
完善了
- +0===-0为true,应该为false
- NaN===NaN为false,应该为true
function myIs(left, right) {
if (left === right) {
return left !== 0 || right !== 0 || 1 / left === 1 / right
} else {
return left !== left && right !== right
}
}