说在前面
最近这段时间一直在整理学习js的手写部分,有哪写的不对的地方,欢迎指出,对哪些有不同见解的,欢迎补充,也希望这些整理能对你有所帮助!!废话就不多说了,直接进入主题吧!!
es5实现let
- 采用自执行函数的形式来定义一个不被污染的变量
(function () {
for (var i = 0; i < 10; i++) {
console.log(i);//0 - 9
}
})()
console.log(i) //a is not defined
es5实现const
- 采用数据劫持defineProperty(只能劫持对象)
- 将所有的变量声明挂载到window对象上
- 由于es5没有块级作用域的概念,无法完整实现const
function _const(name1,value){
window.name1 = value;//挂载到window上便于劫持
Object.defineProperty(window,value,{
enumerable:false,
configurable:false,
get:function(){
return value
},
set: function (newValue) {
throw TypeError("const声明的变量不能修改");
},
})
}
new操作符的实现
- 一个新对象被创建
- 该对象的__proto__指向该构造函数的原型,即fn.prototype
- 将执行上下文(this)绑定到新创建的对象上
- 如果构造函数有返回值(对象或函数),那么这个返回值将取代第一步中新创建的对象
function myNew(fn,...args){
//一个新对象被创建
let res = {};
//该对象的__proto__指向该构造函数的原型,即fn.prototype
if(fn.prototype !== null){
// res.__proto__ = fn.prototype;
Object.setPrototypeOf(res,fn.prototype);
}
//将函数的执行上下文绑定到fn上,并执行
const returnRes = fn.apply(res,args);
//如果构造函数有返回值(对象或函数),那么这个返回值将取代第一步中新创建的对象
if((typeof returnRes == "object" || typeof returnRes == "Function") && returnRes != null){
return returnRes;
}
return res;
}
instanceof 操作符的实现
- 判断隐式原型是否等于显示原型,按原型链一直往上找
function myInstanceof(a,b){
let a1 = a.__proto__;//隐式原型
let b1 = b.prototype;//显示原型
while(a1){
if(a1 != b1){
a1 = a1.__proto__;
}else{
return true
}
}
return false;
}
call、apply、bind实现
call实现
- 获取第一个参数(第一个参数是null或undefined,则this指向window)
- 将this所指向的函数赋给对象
- 执行函数
- 删除传入对象上被赋值的函数
function myCall(obj,...args){
//this指向问题 为null或undefined则指向window
obj = obj ? Object(obj) : window;
//将this所指向的函数赋给对象
obj.fn = this;
//执行函数
let res = obj.fn(...args);
//删除传入对象上被赋值的函数 消除影响
delete obj.fn;
return res;
}
apply实现
- apply和call相差无几,一个传入的是数组,一个传入的是参数(大同小异)
function myCall(obj,arr){
//this指向问题 为null或undefined则指向window
obj = obj ? Object(obj) : window;
//将this所指向的函数赋给对象
obj.fn = this;
//判断是否传入了参数,然后执行函数
let res = arr ? obj.fn(...arr) : obj.fn();
//删除传入对象上被赋值的函数 消除影响
delete obj.fn;
return res;
}
bind实现
- 能够改变this指向
- 返回一个函数
- 能接收多个参数
- 支持柯里化形式传参 fn(arg1)(arg2)
- 获取调用bind()返回值后,若使用new调用(被当作构造函数),bind传入的上下文context失效
- 绑定函数也可以使用new运算符构造,提供的this值会被忽略。新的this指向就应该是new运算符构造出来的this指向
function myBind(context,...args){
if(typeof this != 'Function'){
throw new TypeError('The bound object needs to be a function')
}
// 存下被bind的函数
const self = this;
const fNOP = function(){};
const fBound = function(...fBoundArgs){
// 利用apply改变this调用
// 接受多个参数+支持柯里化形式传参
// 当返回值通过new调用时,this指向当前实例(因为this是当前实例,实例的隐式原型上有fNOP的实例
return self.apply(this instanceof fNOP ? this : context, [...args,...fBoundArgs]);
}
if(this.prototype){
fNOP.prototype = this.prototype;
}
// 通过原型的方式继承调用函数的原型 构造函数继承
fBound.prototype = new fNOP();
return fBound;
}
柯里化实现
- length 存储函数所需要的参数个数
- _args 存储收集到的参数
- _args.length < length 则循环调用收集参数 第一种:
//可能这一版本不太好理解
function curry(fn, args) {
//函数所接收参数的总数
let length = fn.length;
args = args || [];
return function () {
let _args = args;
for (let i = 0; i < arguments.length; i++) {
_args.push(arguments[i]);
}
if (_args.length < length) {
return curry.call(this, fn, _args);//收集参数
}
//收集完参数执行
return fn.apply(this, _args);
}
}
第二种:
function curry(fn, currArgs) {
return function () {
let args = [].slice.call(arguments);
// 首次调用时,若未提供最后一个参数currArgs,则不用进行args的拼接
if (currArgs !== undefined) {
args = args.concat(currArgs);
}
// 递归调用
if (args.length < fn.length) {
return curry(fn, args);
}
// 递归出口
return fn.apply(null, args);
}
}
其实,这两种方法大同小异,主要思路都是收集参数然后运算,细心的同学会发现上面我们提到了bind支持柯里化传参,说明bind的实现就是通过柯里化来实现的,但是也有其局限性,如果只用一个bind他只能实现两层,例如res = add.bind(null,1)(2,3),因为bind返回函数,如果执行完就没了,而真正的柯里化是等参数收集完再执行
第三种:
function add(a, b, c){
return a + b + c
}
let res = add.bind(null,1);
let res1 = res.bind(null,2);
res1(3);//6
扁平化数组
- 去掉中间的那些框框 let arr = [1, [2, 3, [4, 5]]];
第一种:es2019的语法
arr.flat(Infinity);//es2019的语法 最直接
第二种:通过JSON.stringify(arr)方法转为字符串
function flatten(arr){
return JSON.parse(`[${JSON.stringify(arr).replace(/\[|\]/g,'')}]`);
}
第三种:通过concat不断覆盖之前的arr
function flatten(arr){
//Array.some会遍历整个数组
//Array.isArray()判断是否为数组
//每去掉一层[],原数组arr都会发生变化,直到arr中没有数组
while(arr.some(item => Array.isArray(item))){
arr = [].concat(...arr)//不断覆盖arr 不断去掉一层[]
}
return arr;
}
第四种:递归
function flatten(arr){
let res = [];
arr.map(item => {
if(Array.isArray(item)){
res = res.concat(flatten(item))
}else{
res.push(item)
}
})
return res;
}
第五种: reduce方法
该方法其实和concat大同小异,两者都是数组合并
function flatten(arr){
return arr.reduce((res,item) =>{
return res.concat(Array.isArray(item) ? flatten(item) : item)
},[])
}
实现一个对象的扁平化
深浅拷贝
浅拷贝
- 只拷贝第一层,若第一层中有值为引用类型则拷贝其内存地址,也可以这么认为,当你将a的值赋值给b后,修改b中的值a中的值也在变,这就是浅拷贝
第一种:
function copy(item){
if(typeof item !== 'object' && typeof item !== 'null'){
return item;
}
let obj = {};//这里只考虑对象
for(let [ket,value] of Object.entries(item)){
obj[key] = value
}
return obj;
}
第二种:Object.assign() 对象合并
深拷贝
- 这么认为,将a的值赋值给b,当修改b的值时,a的值不会变
第一种:通过JSON.parse()实现
但是其有一定局限性:
- 如果遇到正则表达式的话,其会变为空对象
- 如果value为function的话,则会忽略掉该key
- 如果value为undefined的话,则会忽略掉该key
- 如果value为 NaN、Infinity或-Infinity,则该key会被赋值为null
- 存在循环引用的话,也无法正确深拷贝
function deepCopy(obj){
return JSON.parse(JSON.stringify(obj))
}
第二种:递归实现
- 通过递归判断,如果遇到value为函数则递归
- 存在爆栈的风险(递归调用次数太多,堆栈会有溢出风险)
function deepCopy(obj){
if(typeof obj == 'object' && typeof obj !== null){
let temp = Array.isArray(obj) ? [] : {};//判断类型并创建相应类型
for([key,value] of Object.entries(obj)){
if(typeof value == 'object' && value !== null && Object.prototype.toString.call(value) !== '[object RegExp]'){
temp[key] = deepCopy(value);//value为对象
}else{
temp[key] = value
}
}
return temp
}else{
return obj;
}
}
防抖
- 防止误操作,在一定时间内只执行第一次
- 用闭包实现
function debounce(fn,delay){
let timer = null;//返回一个函数,也将这个属性返回出去,从而形成闭包,垃圾回收机制清除不了
return function(){
let that = this;
let arg = arguments;//类数组(不具备数组的属性)
clearTimeout(timer);//清除定时器
timer = setTimeout(() => {
fn.apply(that,[...arg]);//fn在这被执行,this指向被修改
}, delay);
}
}
节流
- 在规定的时间内只执行一次
- 通过闭包保存一个执行标记
function throttle(fn,delay) {
let canRun = true; // 通过闭包保存一个标记
return function () {
// 在函数开头判断标记是否为true,不为true则return
if (!canRun) return;
// 立即设置为false
canRun = false;
// 将外部传入的函数的执行放在setTimeout中
setTimeout(() => {
// 最后在setTimeout执行完毕后再把标记设置为true(关键)表示可以执行下一次循环了。
// 当定时器没有执行的时候标记永远是false,在开头被return掉
fn.apply(this, arguments);
canRun = true;
}, delay);
};
}
AJAX实现
- 依靠XMLHttpRequest对象与服务端交互--- var ajax = new XMLHttpRequest();
- 初始化 HTTP 请求参数,例如 URL 和 HTTP 方法,但是并不发送请求。 --- ajax.open(method,url)
- 当为post请求时,JSON.stringify(data)处理下数据
- 实现链式调用
function myAjax({ method, url, data }) {
return new Promise((resolve, reject) => {
const ajax = new XMLHttpRequest();
ajax.open(method.toUpperCase(), url, false)
ajax.setRequestHeader("Content-Type", "application/json");
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
resolve(ajax.responseText);
} else {
reject(ajax.responseText)
}
}
ajax.send(method.toUpperCase() === 'POST' ? JSON.stringify(data) : null)
})
}
Promise及其相关方法的实现
Promise实现
-
三个状态且变化之后是不可逆的 pending、rejected、fulfilled
-
promise.then方法中的两个参数是可选的,且两个参数都需要是函数,否则忽略
-
then方法的第一个参数函数执行的条件是状态变为fulfilled,第二个参数函数执行条件是状态为rejected,且都最多只能被调用一次
-
因为then方法中两个参数函数的执行是需要状态改变的,当状态还未改变时需要将then的参数方法放到对应容器中等状态变化后再全部统一执行
例如:
new Promise((resolve, reject) => { setTimeout(() => {//挂起 resolve('你好帅!!'); }, 1000) }).then((res) => {//执行微任务 console.log(res); }) -
Promise能链式调用,所以then方法应该是返回一个Promise对象
-
原生Promise中,如果then方法return返回自身的话会报错
// 三个状态
const RESOLVE = 'fulfilled';//已完成
const REJECT = 'rejected';//已失败
const PENDING = 'pending';//进行中
class myPromise {
status = PENDING;//状态变更
res = undefined;//保存留给then方法用
err = undefined;
onRejectedArr = [];//状态还未变更时,存储回调函数
onResolvedArr = [];
constructor(fn) {
const resolve = (res) => {
if (this.status === PENDING) {
this.res = res;
this.status = RESOLVE;
// 状态变更后调用
this.onResolvedArr.map((fn) => fn());
}
}
const reject = (err) => {
if (this.status === PENDING) {
this.err = err;
this.status = REJECT;
this.onRejectedArr.map((fn) => fn());
}
}
try {
fn(resolve, reject);//执行fn
} catch (err) {
reject(err)
}
}
//then方法
then(onResolved, onRejected) {
//onResolved, onRejected可选参数
//onResolved, onRejected必须作为函数调用
const seft = this;
onResolved = typeof onResolved == 'function' ? onResolved : (res) => res;
onRejected = typeof onRejected == 'function' ? onRejected : (err) => { throw new Error(err) };
// 实现链式调用
const newPromise = new myPromise((resolve, reject) => {
if (seft.status === RESOLVE) {
// .then后的代码需要在自己的执行栈中执行 佯装一下 原生Promise.then是微任务
setTimeout(() => {
try {
const result = onResolved(seft.res);
handlePromise(result, myPromise, resolve, reject);
} catch (error) {
reject(error)
}
}, 0)
}
if (seft.status === REJECT) {
setTimeout(() => {
try {
const result = onRejected(seft.err)
handlePromise(result, myPromise, resolve, reject);
} catch (error) {
reject(error)
}
})
}
// promise里面是异步的话,状态还未变更,先存储到各自的数组中,状态变更后再调用
// 这一点采用了发布订阅模式
if (seft.status === PENDING) {
seft.onResolvedArr.push(() => {//订阅
try {
const result = onResolved(seft.res);
seft.handlePromise(result, myPromise, resolve, reject);
} catch (error) {
reject(error)
}
});
seft.onRejectedArr.push(() => {
try {
const result = onRejected(seft.err);
seft.handlePromise(result, myPromise, resolve, reject);
} catch (error) {
reject(error)
}
});
}
})
// then方法返回的是一个新的Promise实例
return newPromise;
}
//catch方法其实就相当于then(undefined, onRejected)的别名
catch(onRejected) {
return this.then(undefined, onRejected);
}
handlePromise(result, myPromise, resolve, reject) {
// 如果result和myPromise引用的是同一个对象,则抛出错误
// 原生Promise如果return返回自身的话会报错
if (result === myPromise) {
throw new Error('can not return oneseft')
}
// 如果返回的是其他promise,promise也可能是一个构造函数
if ((result && typeof result == 'object') || typeof result == 'function') {
let lock = 'false'//控制then方法里面回调函数的执行
try {
// 是否又then方法
const then = result.then;
// then是一个函数的话就认为是一个promise
if (typeof then === 'function') {
then.call(result,
res => {
if (lock) return;
// then里面可能还有then 这里递归
handlePromise(res, myPromise, resolve, reject);
lock = true;
},
err => {
if (lock) return;
reject(err);
lock = true;
})
} else {
// 不是函数直接返回出去
resolve(result);
}
} catch (error) {
reject(error)
}
} else {
resolve(result);
}
}
}
Promise.resolve()方法实现
- 参数如果是Promise实例,该方法将直接返回该实例
- 如果参数是具有then方法的对象,该方法则会先将其转化为Promise对象,然后执行里面的then方法
- 参数是不具备then方法的对象,或者不是对象,则返回一个新的Promise对象,执行resolve(),状态变为resolved
class Promise {
// ...
//私有方法,只能通过Promise.resolve访问
static resolve(value){
// 参数如果是Promise实例,该方法将直接返回该实例
if(value instanceof Promise) return value;
// 如果参数是具有then方法的对象,该方法则会先将其转化为Promise对象,然后执行里面的then方法
// 然后就立即执行里面的then方法
if(typeof value === 'object' || typeof value === 'function'){
try{
let then = value.then;
// 如果then是函数则立即执行
if(typeof then === 'function'){
return new Promise(then.bind(value));
}
}catch(e){
return new Promise((resolve,reject) => {
reject(e)
})
}
}
// 参数是不具备then方法的对象,或者不是对象,则返回一个新的Promise对象,执行resolve(),状态变为resolved
return new Promise((resolve,reject) => {
resolve(value)
})
}
}
Promise.all()方法实现
- 用于实现多个Promsie运行,返回一个新的Promise实例
- 接收一个具有迭代器的变量(例如数组),里面存放着Promise实例对象
- 如果存放的不是Promise实例对象,则会通过Promise.resolve将其转化为Promise实例
- 当迭代器中所有项的状态全变为fulfilled时,结果的状态才会是fulfilled状态
- 输入的所有promise的resolve回调的结果是一个数组,如果有一个是rejected状态,此时第一个被reject的值,会被立即返回出来
class Promise {
// ...
// 用于将多个Promise实例,包装成一个新的Promise实例,只有所有状态都变为fulfilled,p的状态才会是fulfilled
static all(promiseAll){
const values = [];//存放结果
let resolvedCount = 0;//计数
return new Promise((resolve,reject) => {
promiseAll.forEach((p,index) => {
Promise.resolve(p).then(value =>{
resolvedCount++;
values[index] = value;
if(resolvedCount == promiseAll.length){//当全变为fulfilled,返回结果数组
resolve(values);
}
},err => {
reject(err)//有错误则返回
})
});
})
}
}
Promise.rave()方法实现
- 参数必须是一个可迭代器对象
- 该方法返回一个Promsie,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。
- 结果可以是完成( resolves),也可以是失败(rejects),这要取决于第一个完成的方式是两个中的哪个。
- 如果传的迭代是空的,则返回的 promise 将永远等待。
- 如果迭代包含一个或多个非承诺值和/或已解决/拒绝的承诺,则 Promise.race 将解析为迭代中找到的第一个值。
顾名思义竞赛
class Promise {
// ...
static race(promise){
return new Promise((resolve,reject) => {
promise.forEach((p,index) => {
Promise.resolve(p).then(value => {
resolve(value);
},err => {
reject(err)
})
})
})
}
}
写在后面
其实手写类的题还有好多好多(例如:对象的扁平化、对象扁平化恢复),但是篇幅有限,我所总结的也只是冰山一角,也希望大佬们告诉我还有哪些,咱一起卷!!!