原生 JS 方法的实现
总结一下一些操作符或者方法的原生实现。持续更新
new-操作符
创建某个实例,必须使用 new 操作符。这种方式调用构造函数会经历以下 4 个步骤:
- 创造一个新的对象
- 将构造函数的作用域赋值给新对象(this 指向该对象)
- 执行构造函数中的代码(为新对象添加属性)
- 返回新对象
function myNew(fun) {
var obj = Object.create(fun.prototype);
//var obj = {}
//obj.__proto__ = fun.prototype 与上述方法实现效果相同
var args = [...arguments].slice(1);
var res = fun.call(obj, ...args);
return typeof res === 'object' ? res : obj;
}
instanceof 操作符
instanceof 可以正确的判断对象的类型,因为内部的机制是通过判断对象的原型链中是不是能够找到类型的 prototype。
function myInstanceof(left, right) {
if (typeof left !== 'object' || typeof left !== 'function') {
return false;
}
let prototype = right.prototype;
left = left.__proto__;
while (true) {
if (left === null) {
return false;
}
if (prototype === left) {
return true;
}
left = left.__proto__;
}
}
call / apply / bind 的实现
call func.call(obj, args1, args2); //传入的是参数列表
apply func.apply(obj, [args1, args2]); //传入的是数组参数 记法:apply是a开头为array
bind func.bind(obj, args1, args2); //返回的是函数,前两者为立即执行
中心思想
// 改变this的指向,让bar()函数的this指向foo对象
var foo = {
value: 100
}
function bar() {
console.log(this.value);
}
bar.apply(foo);
//等同与将bar函数作为foo对象属性进行调用,即为下列方式
var foo = {
value: 100
bar: function() {
console.log(this.value);
}
}
/*
* 因此我们总结模拟思路如下:
* 1.将执行的函数作为对象属性
* 2.执行函数
* 3.将函数从对象中删除
*/
call
Function.prototype.myCall = function (context) {
//使用该方式在原型链上拓展方法,以便所有的函数都能够使用.方式调用
if (typeof this !== 'function') {
//判断调用是否为函数
throw new TypeError('Error');
}
const ctx = context || window; //取到传入的对象(执行上下文),如果不传参数默认指向window
ctx.func = this; //给obj对象添加一个func方法,this也就是调用myCall的函数
const args = [...arguments].slice(1); //取得传入除了obj的参数
const result = ctx.func(...args); //执行函数
delete ctx.func; //删除函数
return result;
};
apply
Function.prototype.myApply = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error');
}
const ctx = context || window;
ctx.func = this;
let result;
if (arguments[1]) {
//判断是否传入数组参数,如果传入就要展开数组
result = ctx.func(...arguments[1]);
} else {
result = ctx.func();
}
delete ctx.func;
return result;
};
bind
相对于前两个,这个函数返回的函数,并不是立即执行。
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error');
}
context = context || window;
const _this = this;
const args = [...arguments].slice(1);
return function F() {
return _this.apply(context, args.concat(...arguments));
};
};
flat 的实现
递归
// 判断当前数据是否为数组,是则递归
function flatten(nums) {
let result = [];
nums.forEach((item) => {
if (Array.isArray(item)) {
result = result.concat(flatten(item));
} else {
result.push(item);
}
});
return result;
}
toString + map
// 这种方式比较局限,只有数组中元素为数字或者数字字符时,如果有‘a’这类字符串就会出错
function flatten(nums) {
return nums
.toString()
.split(',')
.map((item) => +item);
}
reduce
// 思路与递归相似,判断当前是否为数组
function flatten(nums) {
return nums.reduce((pre, curr) => {
return pre.concat(Array.isArray(curr) ? flatten(curr) : curr);
}, []);
}
some + 扩展运算符
// 扩展运算符可以平铺开一层数组,如果一直存在数组的话,一直采用...平铺数组,直至没有数组
function flatten(nums) {
while (nums.some((item) => Array.isArray(item))) {
nums = [].concat(...nums);
}
return nums;
}
层数控制
function isArray(arr) {
return Array.isArray(arr);
}
function flatten(arr) {
var result = [];
var deep = arguments[1] || 1; //判断是否传入展平 层级
arr.forEach((element) => {
if (isArray(element) && deep >= 1) {
result = result.concat(flatten(element, deep - 1));
} else {
result.push(element);
}
});
return result;
}
防抖节流
在 resize/scroll/keypress/mousemove 等事件触发时,会不断调用回调函数。
在我们实现搜索的时候,输入搜索内容进行模糊搜索的时候,也会不断调用回调函数,这样子性能就被降低了。
对于这类事件的处理方式就是使用事件节流和事件防抖。它们通过对事件对应的回调函数进行包裹,以自由变量的形式缓存时间信息。
防抖(debounce)
假设设置时间为 2000ms,如果触发事件的 2000ms 之内,你再次触发该事件,就会给新的事件添加新的计时器,之前的事件统统作废。只执行最后一次触发的事件。
在某段时间内,不管你触发了多少次事件,我都只认最后一次。
function debounce(fn, delay) {
let timer = null;
return function () {
let _this = this;
let args = arguments;
clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(_this, args);
}, delay);
};
}
节流(throttle)
在某段时间内,不管你触发了多少次事件,我都只认第一次,并在计时结束时给予响应。 假设设置的时间为 2000ms,再触发了事件的 2000ms 之内,你在多少触发该事件,都不会有任何作用,它只为第一个事件等待 2000ms。时间一到,它就会执行了。
时间戳
设置触发事件时的时间戳,然后减去之前的时间戳,如果大于时间周期,则执行函数,更新时间戳。
function throttle(fn, delay) {
//点击第一下就会立即执行
let pre = 0;
return function () {
let _this = this;
let args = arguments;
let now = new Date();
if (now - pre > delay) {
fn.apply(_this, args);
pre = now;
}
};
}
定时器
设置一个定时器,再次触发时,如果存在定时器就不会执行函数,知道定时器执行之后,清空定时器,才能够设置下一个定时器。
function throttle(fn, delay) {
let timer;
return function () {
const _this = this;
const args = [...arguments];
if (!timer) {
timer = setTimeout(function () {
timer = null;
fn.apply(_this, args);
}, delay);
}
};
}
两者比较
- 时间戳方式,会立即执行第一个事件;定时器方式,会隔 n 秒后执行第一个事件。
- 时间戳方式,停止触发后不会再执行事件;定时器方式,停止触发后会再一次执行事件。
jsonp 的实现
jsonp 的作用是解决跨域问题。 因为 script 标签不受同源策略限制,所以可以动态插入 script 实现跨域。
function jsonp(url, data) {
data = data || {};
return new Promise((resovle, reject) => {
const cbFn = `jsonp_${Date.now()}`;
data.callback = cbFn;
const head = document.querySelector('head');
const script = document.createElement('script');
const src = `${url}?${dataToUrl(data)}`;
script.src = src;
head.append(script);
window[ncbFn] = (res) => {
resovle(res);
head.removeChild(script);
window[cbFn] = null;
};
});
}
function dataToUrl(data) {
return Object.keys(data)
.reduce((pre, curr) => {
pre.push(`${curr}=${data[curr]}`);
return pre;
}, [])
.join('&');
}
jsonp('http://jsonplaceholder.typicode.com/comments', {
postId: 1,
}).then((res) => {
console.log(res);
});
函数柯里化
当某个函数一次需要接受多个参数时,可以使用柯里化来使得当前函数一次性接受少一点的参数。
百科:把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
function carry(fn) {
const args = [...arguments].slice(1);
if (fn.length <= args.length) {
//当传入参数的数量>=函数参数数量时,执行函数
return fn(...args);
}
return function () {
//返回一个函数,等待传入其他参数
const args1 = [...arguments];
return carry(fn, ...args, ...args1);
};
}
function add(a, b, c) {
return a + b + c;
}
const carryAdd = carry(add);
carryAdd(1, 2, 3);
carryAdd(1)(2, 3);
carryAdd(1)(2)(3);
carryAdd(1)()(2)()(3);
LazyMan(链式调用)
function LazyMan(name) {
return new _LazyMan(name);
}
function _LazyMan(name) {
this.name = name;
this.queue = []; //初始化队列
this.queue.push(() => {
console.log('Hi, ' + name);
this.next();
});
setTimeout(() => {
this.next();
}, 0);
}
_LazyMan.prototype.next = function () {
const fn = this.queue.shift();
fn && fn();
};
_LazyMan.prototype.eat = function (food) {
this.queue.push(() => {
console.log(this.name + ' eat ' + food);
this.next();
});
return this;
};
_LazyMan.prototype.sleep = function (time) {
this.queue.push(() => {
setTimeout(() => {
console.log(this.name + ' sleep ' + time + 's');
this.next();
}, time * 1000);
});
return this;
};
数组去重
双重循环(兼容性好)
function unique(nums) {
var result = [];
for (var i = 0; i < nums.length; i++) {
for (var j = 0; j < result.length; j++) {
if (nums[i] === result[j]) {
break;
}
}
if (j === result.length) {
result.push(nums[i]);
}
}
return result;
}
indexOf(简化上一个方法的内部操作)
function unique(nums) {
var result = [];
for (var i = 0; i < nums.length; i++) {
if (result.indexOf(nums[i]) === -1) {
result.push(nums[i]);
}
}
return result;
}
reduce + sort 实现(先排序在去重)
Array.prototype.unique = function () {
const arr = this;
return arr
.sort((a, b) => a - b)
.reduce((pre, curr) => {
if (pre.length === 0 || pre[pre.length - 1] !== curr) {
pre.push(curr);
}
return pre;
}, []);
};
对象实现
可以使用普通对象,或者使用 ES6 Map
Array.prototype.unique = function () {
const map = new Map(); //只能存在一个key
return this.filter((item) => {
return !map.has(item) && map.set(item, 1);
});
};
Array.prototype.unique = function () {
const obj = {};
return this.filter((item) => {
return obj.hasOwnProperty(item) ? false : (obj[item] = true);
});
};
类型判断
typeof(能够判断基本类型)
基本类型:number / string / null / undefined / boolean / symbol(ES6) 无法区分是哪种类型的对象
typeof null; //"object"
typeof {}; //"object"
typeof []; //"object"
typeof function foo() {}; //"function"
Object.prototype.toString()
Object.prototype.toString 会返回一个由 “[object “ 和 class 和 “]” 组成的字符串
- 如果 this 值是 undefined,就返回 [object Undefined]
- 如果 this 的值是 null,就返回 [object Null]
- 让 O 成为 ToObject(this) 的结果
- 让 class 成为 O 的内部属性 [[Class]] 的值
- 最后返回由 “[object “ 和 class 和 “]” 三个部分组成的字符串
常规 11 种数据类型:Boolean / Number / String / Function / Array / Date / RegExp / Object / Error / Null / Undefined
类型判断函数
function type(obj) {
const classTypeMap = {};
'Boolean Number String Function Array Date RegExp Object Error'
.split(' ')
.forEach((item) => {
classTypeMap['[object ' + item + ']'] = item.toLowerCase();
});
if (obj === null) {
return obj + '';
}
return typeof obj === 'object' || typeof obj === 'function'
? classTypeMap[Object.prototype.toString.call(obj)]
: typeof obj;
}
深浅拷贝
JS 有两种数据类型
基本数据类型 string/number/boolean/undefined/null/symbol(ES6) 引用数据类型 object/array/function
浅拷贝
对于基本数据类型来说,拷贝一份互不影响;而对于数组和对象来说,只会拷贝数组或者对象的引用,对新旧数组进行修改,两个数组都会发生变化。
基础的数组方法能够实现浅拷贝,例如 concat()、slice();对象能够使用 Object.assign()实现浅拷贝;两者同是都可以使用扩展运算符实现浅拷贝。
// 如果是嵌套数组和对象,上述方法不可行
let arr = [1, 2, 3, 4],
arr1 = [1, 2, 3, [4, 5, 6]];
let copyarr = arr.slice(),
copyarr1 = arr1.slice();
arr[1] = 5; //arr[1, 5, 3, 4], copyarr[1, 2, 3, 4]
arr1[3][1] = 7; //arr1[1, 2, 3, [4, 7, 6]], copyarr1[1, 2, 3, [4, 7, 6]]
let obj = { a: 1, b: 2, c: 3 },
obj1 = { a: 1, b: 2, c: { d: 3, e: 4 } };
let copyobj = Object.assign({}, obj),
copyobj1 = Object.assign({}, obj1);
obj.b = 4; //obj{ a: 1, b: 4, c: 3 }, copyobj{ a: 1, b: 2, c: 3 }
obj1.c.d = 6; //obj1{ a: 1, b: 2, c: { d: 6, e: 4 } }, copyobj1{ a: 1, b: 2, c: { d: 6, e: 4 } }
深拷贝
指完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。
可以使用 JSON 的两个函数方法实现深拷贝
let arr = ['old', 1, true, ['old1', 'old2'], { old: 1 }];
let new_arr = JSON.parse(JSON.stringify(arr));
arr[3][1] = 'old4'; //new_arr['old', 1, true, ['old1', 'old2'], {old: 1}]
但是缺点就是不能拷贝函数和 undefined
深拷贝函数
function deepClone(obj) {
function isObject(obj) {
//判断是不是对象
return (
(typeof obj !== null && typeof obj === 'object') ||
typeof obj === 'function'
);
}
if (!isObject(obj)) {
return;
}
let newObj = Array.isArray(obj) ? [] : {}; //判断对象类型
Object.keys(obj).forEach((key) => {
newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]; //判断当前值是不是对象
});
return newObj;
}
进阶版深拷贝函数
上面实现的 deepClone 有一个问题,当我们对这样一个对象进行深拷贝时,会报错。
const target = {
filed1: 1,
filed2: 2,
filed: [1, 2, 3],
};
target.target = target;
在对 target 对象进行深拷贝,报错调用栈溢出 Maximum call stack size exceeded。导致这个的原因是对象的属性间接或直接的引用了自身的情况。 为了解决这个问题,我们可以开辟一个新的空间来存储当前对象和拷贝对象的关系,当需要拷贝当前对象时,先去存储空间找是否拷贝过;如果有直接返回,如果没有就继续拷贝。
function deepClone(obj, map = new Map()) {
function isObject(obj) {
//判断是不是对象
return (
(typeof obj !== null && typeof obj === 'object') ||
typeof obj === 'function'
);
}
if (isObject(obj)) {
let newObj = Array.isArray(obj) ? [] : {}; //判断对象类型
if (map.get(obj)) {
//如果已经克隆过了,直接返回
return map.get(obj);
}
map.set(obj, newObj); //否则将当前对象作为key,克隆对象作为value存储
Object.keys(obj).forEach((key) => {
newObj[key] = isObject(obj[key]) ? deepClone(obj[key], map) : obj[key]; //判断当前值是不是对象
});
return newObj;
} else {
return obj;
}
}
可以把 map 改为 weakMap,采用弱引用。
JSON.stringfy
JSON.stringfy 是常用到的 JSON 对象中的一个方法。 JSON 对象包含的两个方法:
- JSON.parse(): 解析成 JSON 对象 将对象解析成为 JSON 字符串
- JSON.stringify 方法是将一个 JavaScript 对象或值转换为 JSON 字符串,默认该方法其实有三个参数:第一个参数是必选,后面两个是可选参数非必选。第一个参数传入的是要转换的对象;第二个是一个 replacer 函数,比如指定的 replacer 是数组,则可选择性地仅处理包含数组指定的属性;第三个参数用来控制结果字符串里面的间距,后面两个参数整体用得比较少。 该方法的语法为:JSON.stringify(value[, replacer [, space]])
针对于 JSON.stringfy 来说,内部处理数据时有以下特点
- 转换值有 toJSON 方法直接调用
- 在基本数据类型中,undefined/symbol/function 处理为 undefined,null/Infinity 处理为 “null”
- 在 Array 中,undefined/symbol/function 处理为 “null”;在非数组对象中,undefined/symbol/function 会被忽略
- 所有以 symbol 为属性键的属性都会被完全忽略掉
function jsonStringify(data) {
let type = typeof data;
//基础类型或者function
if (type !== 'object') {
let result = data;
//处理NaN和Infinity
if (Number.isNaN(data) || data === Infinity || data === -Infinity) {
return 'null';
}
//处理function/undefined/symbol
else if (type === 'function' || type === 'undefined' || type === 'symbol') {
return undefined;
}
//处理string
else if (type === 'string') {
result = `"${data}"`;
}
return String(result);
}
//引用类型或者null
else {
//处理null
if (data === null) {
return 'null';
}
//如果自带toJSON()方法,直接调用
else if (data.toJSON && typeof data.toJSON === 'function') {
return jsonStringify(data.toJSON());
}
//处理Array
else if (Array.isArray(data)) {
let result = [];
data.forEach((item, index) => {
if (type === 'function' || type === 'undefined' || type === 'symbol') {
result[index] = 'null';
} else {
result[index] = jsonStringify(item);
}
});
result = `[${result}]`;
return result.replace(/'/g, '"');
}
//处理普通对象
else {
let result = [];
Object.keys(data).forEach((item) => {
//key 如果是 symbol,忽略
if (typeof item !== 'symbol') {
//键值如果是 undefined、function、symbol 为属性值,忽略
if (
data[item] !== undefined &&
typeof data[item] !== 'function' &&
typeof data[item] !== 'symbol'
) {
result.push(`"${item}":${jsonStringify(data[item])}`);
}
}
});
return `{${result}}`.replace(/'/g, '"');
}
}
}
EventEmit 发布订阅模式
实现自定义事件的订阅与发布,主要包含 on、off、emit、once、alloff 方法
- on 指定事件添加的监听器可以持续不断地监听相应的事件
- once 添加的监听器,监听一次后,就会被消除
- off 删除指定事件的指定监听器
- allOff 删除指定事件的全部监听器
- emit 执行指定事件的全部监听器
function EventEmit() {
this.__event = {};
}
EventEmit.prototype.on = function (eventName, listener) {
function isValidListener(listener) {
if (typeof listener === 'function') return true;
else if (typeof listener === 'object') {
return isValidListener(listener.listener);
} else {
return false;
}
}
if (!eventName || !listener) return;
if (!isValidListener(listener)) {
throw new TypeError('listener must be a function');
}
var events = this.__event;
var listeners = (events[eventName] = events[eventName] || []);
listeners.push(listener);
return this;
};
EventEmit.prototype.once = function (eventName, listener) {
return this.on(eventName, { listener, once: true });
};
EventEmit.prototype.emit = function (eventName, arg) {
var that = this;
var listeners = this.__event[eventName];
if (!listeners) return;
for (var i = 0; i < listeners.length; i++) {
const listener = listeners[i];
if (!listener) continue;
if (typeof listener === 'function') {
listener.apply(that, arg);
} else {
listener.listener.apply(that, arg);
that.off(eventName, listener.listener);
}
}
return this;
};
EventEmit.prototype.off = function (eventName, listener) {
var listeners = this.__event[eventName];
if (!listeners) return;
for (var i = 0, len = listeners.length; i < len; i++) {
if (
listeners[i] &&
(listeners[i] === listener || listeners[i].listener === listener)
) {
listeners.splice(i, 1, null);
break;
}
}
return this;
};
EventEmit.prototype.allOff = function (eventName) {
if (eventName && this.__event[eventName]) {
this.__event[eventName] = [];
}
return this;
};
Array.prototype.map
map() 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
Array.prototype.map = function (callback, thisArg) {
if (this === null) {
throw new TypeError("Cannot read property 'map' of null");
}
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
//获取当数据和当前this
let O = Object(this);
let T = thisArg;
let len = O.length >>> 0;
let A = new Array(len); //创建新的数据
let k = 0;
while (k < len) {
if (k in O) {
let kValue = O[k]; //当前数据
A[k] = callback.call(T, kValue, k, O);
}
k++;
}
return A;
};