JS手写代码系列总结

393 阅读9分钟

参考文章:

JS常见手写代码题(一)

JS常见手写代码题(二)

【javascript】手写call,apply,bind函数

32个手写JS_单眼皮的小熊-CSDN博客

本文从以上文章中,摘录总结了一部分常见且需要好好理解一下的JS手写代码题型,面试必备,按需选取

包括以下这些,更多更详细的解释可查阅以上原文出处

  1. call
  2. apply
  3. bind
  4. new
  5. 数组扁平化
  6. 数组去重
  7. 原型继承
  8. promise、promise.all、promise.race
  9. instanceof
  10. ajax
  11. 闭包cache
  12. 浅拷贝、深拷贝
  13. 字符串转驼峰
  14. 图片懒加载
  15. 滚动加载

一、JS实现一个call

方法或函数fun.call(obj, 参数1,参数2,...),第一个值是改变this指向到obj,后面是参数队列,调用call立即执行方法fun

call的定义和用法

// call方法第一个参数指的是this的指向;接受一个参数列表;方法立即执行
// Function.prototype.call()样例
function fun(arg1, arg2) {
  console.log(this.name)
  console.log(arg1 + arg2)
}
const _this = { name: 'YIYING' }
// 这里把fun里的this,指向对象_this,然后立即执行,由此才可以输出YIYING
fun.call(_this, 1, 2)
// 输出
YIYING
3

手写实现call

Funcion.protoType.mockCall = function (context = window, ...args) {
	const key = Symbol()
	context[key] = this
	const result = context[key](...args)
	delete context[key]
	return result
}
或者:
Function.prototype.myCall = function(context) {
    if (typeof context === "undefined" || context === null) {
        context = window
    }
   //context=context||window  和上面的代码一样
    context.fn = this//(因为call的调用方式形如:myFun.call(obj),因此此时call方法的this指向为myFun,因此context.fn = this即为context.fn = myFun)
    const args = [...arguments].slice(1)//第一个参数为context,要去除
    const result = context.fn(...args)
    delete context.fn
    return result
}

实现分析

  • 首先context为可选参数,如果不传的话默认上下文是window
  • 接下来给content创建一个独一无二的属性(Symbol表示),并将值设置为需要调用的函数
  • 因为call可以传入多个参数作为调用函数的参数,这里用的...扩展运算符
  • 然后调用函数并将对象上的函数删除

二、JS实现一个apply

方法或函数fun.apply(obj, [参数1,参数2,...]),改变this指向到obj,立即执行方法fun

apply接受两个参数,第一个参数是要绑定给this的值,第二个参数是一个**参数数组。**apply和call实现类似,不同的就是参数的处理

Function.protoType.mockApply = function (context = window, args) {
	const key = Symbol()
	context[key] = this
	const result = context[key](...args)
	delete context[key]
	return result
}

三、JS实现一个bind

Function.prototype.bind 第一个参数是this的指向,从第二个参数开始是接收的参数列表。和call的区别在于bind方法返回值是函数以及bind接收的参数列表的使用。

实现思路:

  • 利用闭包保存调用bind时的this,这时的this就是原函数
  • 使用call/apply指定this
  • 返回一个绑定函数
  • 当返回的绑定函数被new运算符调用的时候,绑定的上下文指向new运算符创建的对象
  • 将绑定函数的prototype修改为原函数的prototype
Function.protoType.mockBind = function (context = window, ...initArgs) {
	const foo = this
	var bindFoo = function (...args) {
		if(this instanceof bindFoo){
      return new fn(...initArgs, ...args)
    }
		return foo.call(context, ...initArgs, ...args)
	}
	return bindFoo
}
简写:
Function.prototype.mockBind = function(ctx){
    let fn = this
    return function(){
        fn.apply(ctx, arguments) //arguments是函数调用时所传参数
    }  
}

第一个参数是this的指向,从第二个参数开始是接收的参数列表。区别在于bind方法返回值是函数以及bind接收的参数列表的使用。

四、手写一个new的实现

正常使用new

function Dog(name){
    this.name = name
}
Dog.prototype.sayName = function(){
    console.log('名字', this.name)
}
var dog1 = new Dog('小狗')
dog1.sayName() // 输出名字 小狗

思考一下new 操作符做了哪些事情?

  • 创建一个新对象
  • 新对象会被执行 __proto__链接,关联到构造函数的.prototype 属性上,即和构造函数用的一个原型,从而可调用原型上的方法
  • 函数调用的this绑定到新对象上
  • 如果函数没有返回其他对象,那么new表达式中的函数会调用自动返回这个新对象

手写new实现

function mockNew (foo, ...args) {
	if (typeof foo !== 'function') {
    throw Error('foo is not a constructor')
  }
	const obj = Object.create(foo.protoType)
	const result = foo.apply(obj, args)
	return typeOf result === 'object' && result !== null ?  result : obj
}
new的具体步骤
1. 创建一个空对象 var obj = {}
2. 修改obj.__proto__=Dog.prototype
3. 只改this指向并且把参数传递过去,call和apply都可以
4. 根据规范,返回 nullundefined 不处理,依然返回obj

五、数组扁平化

方法一:es6 flat方法

var arr = [1,2,[3,4,[5,6,[7]]]]
arr.flat(Infinity) // [1,2,3,4,5,6,7]

方法二:递归

var flatArr = function(arr1) {
	let newArr = [];
  function getChild(arr) {
		for(let i = 0; i<=arr.length;i++) {
			if(arr[i] instanceof Array === false && arr[i]) {
				newArr.push(arr[i])
			} else if(arr[i]){
				getChild(arr[i])
			}
		}
	}
	getChild(arr1);
	return newArr;
}

// 调用:
var a = [[1,2,2], [6,7,8, [11,12, [12,13,[14]]], 10]];
console.log('水电费', flatArr(a))
// [1, 2, 2, 6, 7, 8, 11, 12, 12, 13, 14, 10]

方法三:正则

const res2 = JSON.stringify(arr).replace(/\[|\]/g, '').split(',');
res2.map(item=> parseInt(item))

六、数组去重

方法一:es6 Set

var arr = [1,2,3,3,4,4,5]
var newArr = Array.from(new Set(arr)); // [1,2,3,4,5]
// 或者arr = [...set]      Array.from() 将伪数组转换为数组

方法二:循环遍历数组

function filterArr(arr){
	var newArr = [];
	arr.forEach(item => {
		if(!newArr.includes(item)) { // 也可以是!newArr.indexOf(item)
			newArr.push(item)
		}
	})
	return newArr
}

方法三:hash表

let arr = [1,1,2,3,2,1,2]
function unique(arr){
    let obj = {}
    arr.forEach((item) => {
        obj[item] = true
    })
    let keys = Object.keys(obj)
    keys = keys.map(item => parseInt(item)) // 转为数字
    return keys
}
console.log(unique(arr))

七、原型继承(寄生组合继承)

这里只写寄生组合继承了,中间还有几个演变过来的继承但都有一些缺陷
function Parent() {
  this.name = 'parent';
}
function Child() {
  Parent.call(this);
  this.type = 'children';
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

八、手写一个Promise

完整解析(写的很好,值得一看):

zhuanlan.zhihu.com/p/103651968

function Promise(executor) {
let self = this
this.status = 'pending' //当前状态
this.value = undefined  //存储成功的值
this.reason = undefined //存储失败的原因
this.onResolvedCallbacks = []//存储成功的回调
this.onRejectedCallbacks = []//存储失败的回调
function resolve(value) {
  if (self.status == 'pending') {
    self.status = 'resolved'
    self.value = value
    self.onResolvedCallbacks.forEach(fn => fn());
  }
}
function reject(error) {
  if (self.status == 'pending') {
    self.status = 'rejected'
    self.reason = error
    self.onRejectedCallbacks.forEach(fn => fn())
  }
}
try {
  executor(resolve, reject)
} catch (error) {
  reject(error)
}
}
Promise.prototype.then = function (infulfilled, inrejected) {
let self = this
let promise2
infulfilled = typeof infulfilled === 'function' ? infulfilled : function (val) {
  return val
}
inrejected = typeof inrejected === 'function' ? inrejected : function (err) {
  throw err
}
if (this.status == 'resolved') {
  promise2 = new Promise(function (resolve, reject) {
    //x可能是一个promise,也可能是个普通值
    setTimeout(function () {
      try {
        let x = infulfilled(self.value)
        resolvePromise(promise2, x, resolve, reject)
      } catch (err) {
        reject(err)
      }
    });

  })
}
if (this.status == 'rejected') {

  promise2 = new Promise(function (resolve, reject) {
    //x可能是一个promise,也可能是个普通值
    setTimeout(function () {
      try {
        let x = inrejected(self.reason)
        resolvePromise(promise2, x, resolve, reject)
      } catch (err) {
        reject(err)
      }
    });
  })
}
if (this.status == 'pending') {
  promise2 = new Promise(function (resolve, reject) {
    self.onResolvedCallbacks.push(function () {
      //x可能是一个promise,也可能是个普通值
      setTimeout(function () {
        try {
          let x = infulfilled(self.value)
          resolvePromise(promise2, x, resolve, reject)
        } catch (err) {
          reject(err)
        }
      });
    })
    self.onRejectedCallbacks.push(function () {
      //x可能是一个promise,也可能是个普通值
      setTimeout(function () {
        try {
          let x = inrejected(self.reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (err) {
          reject(err)
        }
      });
    })
  })
}
return promise2
}
function resolvePromise(p2, x, resolve, reject) {
if (p2 === x && x != undefined) {
  reject(new TypeError('类型错误'))
}
//可能是promise,看下对象中是否有then方法,如果有~那就是个promise
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
  try {//为了防止出现 {then:11}这种情况,需要判断then是不是一个函数
    let then = x.then
    if (typeof then === 'function') {
      then.call(x, function (y) {
        //y 可能还是一个promise,那就再去解析,知道返回一个普通值为止
        resolvePromise(p2, y, resolve, reject)
      }, function (err) {
        reject(err)
      })
    } else {//如果then不是function 那可能是对象或常量
      resolve(x)
    }
  } catch (e) {
    reject(e)
  }
} else {//说明是一个普通值
  resolve(x)
}
}

Promise.all

Promise.all是支持链式调用的,本质上就是返回了一个Promise实例,通过resolvereject来改变实例状态。

Promise.myAll = function(promiseArr) {
  return new Promise((resolve, reject) => {
    const ans = [];
    let index = 0;
    for (let i = 0; i < promiseArr.length; i++) {
      promiseArr[i]
      .then(res => {
        ans[i] = res;
        index++;
        if (index === promiseArr.length) {
          resolve(ans);
        }
      })
      .catch(err => reject(err));
    }
  })
}

Promise.race

Promise.race = function(promiseArr) {
  return new Promise((resolve, reject) => {
    promiseArr.forEach(p => {
      // 如果不是Promise实例需要转化为Promise实例
      Promise.resolve(p).then(
        val => resolve(val),
        err => reject(err),
      )
    })
  })
}

九、实现检测数据类型的instanceof

left表示要检测的数据,right表示类型。其原理是用原型链实现的,
A(实例对象) instanceof B(构造函数)。
function instanceof(left, right){
    let proto = left._proto_
    let prototype = right.prototype
    while(true){
        if(proto === null) return false
        if(proto === prototype) return true
        proto = proto._proto_
    }
}

十、ajax

(1)get请求的ajax

let xhr = new XMLHttpRequest() //1、创建连接
xhr.open('GET', url, true) //2、连接服务器
xhr.onreadystatechange = function () { //4、接收请求,当状态改变时触发这个函数
    if (xhr.readyState === 4) {
        if (xhr.status === 200) {//xhr.responseText是字符串需转换为JSON
            success(JSON.parse(xhr.responseText))
        } else {
            fail(xhr.status)
        }
    }
}
xhr.send(null) //3、发送请求

(2)post请求的ajax

let xhr = new XMLHttpRequest() //1、创建连接
const postData = {
    userName: 'zhangshan',
    passWord: 'xxx'
}
xhr.open('POST', url, true) //2、连接服务器
xhr.onreadystatechange = function () { //4、接收请求,当状态改变时触发这个函数
    if (xhr.readyState === 4) {
        if (xhr.status === 200) {//xhr.responseText是字符串需转换为JSON
            success(JSON.parse(xhr.responseText))
        } else {
            fail(xhr.status)
        }
    }
}
xhr.send(JSON.stringify(postData)) //3、发送请求(需发送字符串,将json转化成字符串)

(3)用Promise优化

function ajax(url) {
    return new Promise((resolve, reject) => {
        let xhr = new XMLHttpRequest() //1、创建连接
        xhr.open('GET', url, true) //2、连接服务器
        xhr.onreadystatechange = function () { //4、接收请求,当状态改变时触发这个函数
            if (xhr.readyState === 4) {
                if (xhr.status === 200) {//xhr.responseText是字符串需转换为JSON
                    resolve(JSON.parse(xhr.responseText))
                }else if(xhr.status === 404){
                    reject(new Error('404'))
                }
            }
        }
        xhr.send(null) //3、发送请求
    })
} 
const url = ''
ajax(url)
.then(res => console.log(JSON.parse(xhr.responseText)))
.catch(err => console.log(err))

十一、闭包写一个cache工具

function creatCache() {
    let data = {} //隐藏数据,外部访问不到
    return {
        get(key) {
            return data[key]
        },
        set(key, val) {
            data[key] = val
        }
    }
}
var c = creatCache()
c.set('name', 'jin')
console.log(c.get('name'))

十二、浅拷贝、深拷贝

浅拷贝只复制对象的第一层属性、深拷贝是对对象的属性进行递归复制。

//浅拷贝   (obj1为所要拷贝的对象)
//方式一:原始版本(obj1为所要拷贝的对象,obj2已经默认为一个对象)
function shallowCopy(obj1, obj2){
    for(let key in obj1){
        obj2[key] = obj1[key]
    }
}
//方式一:优化版本(obj为所要拷贝的对象)
function shallowClone(obj){
    if(typeof obj !== 'object' || obj == null){
        //obj是null,或者不是对象和数组,直接返回
        return obj
    }
    let result
    if(obj instanceof Array){
        result = []
    }else{
        result = {}
    }
    for(let key in obj){// for in 遍历对象可枚举属性,包括其原型的属性和方法, 
        if(obj.hasOwnProperty(key)){ //保证key不是原型的属性
            //递归调用
            result[key] = obj[key]
        }
    }
    //返回结果
    return result
}
//方式二
function shallowCopy(obj1, obj2){
    obj2 = Object.assign({}, obj1)
}
//深拷贝   (obj1为所要拷贝的对象)
//方式一:原始版本(obj1为所要拷贝的对象)   
function deepCopy(obj1, obj2){
    for(let key in obj1){// for in 遍历对象可枚举属性,包括其原型的属性和方法, 可用obj1.hasOwnPerporty(key)判断这个实例是否有这个属性
        let item = obj1[key] 
        if(item instanceof Array){ // 不能用typeof  item,因为不能区分对象和数组
            obj2[key] = []
            deepCopy(item, obj2[key])
        }else if(item instanceof Object){
            obj2[key] = {}
            deepCopy(item, obj2[key])
        }else{
            obj2[key] = item
        }
    }
}
//方式一:优化版本(obj为所要拷贝的对象,obj2已经默认为一个对象)
思路:1、判断是否是值类型还是引用类型。2、判断是数组还是对象。3、递归
function deepClone(obj){
    if(typeof obj !== 'object' || obj == null){
        //obj是null,或者不是对象和数组,直接返回
        return obj
    }
    let result
    if(obj instanceof Array){
        result = []
    }else{
        result = {}
    }
    for(let key in obj){// for in 遍历对象可枚举属性,包括其原型的属性和方法, 
        if(obj.hasOwnProperty(key)){ //保证key不是原型的属性
            //递归调用
            result[key] = deepClone(obj[key])
        }
    }
    //返回结果
    return result
}
缺陷:当遇到两个互相引用的对象,会出现死循环的情况。
//方式二  
function deepCopy(obj1, obj2){
    obj2 = JSON.parse(JSON.stringify(obj1))
}
缺陷:这种方法不能拷贝函数属性

十三、字符串转驼峰

方法一:分割成数组,利用toUpperCase()转大写,substring(1)为第一个元素后面的元素
var str="border-bottom-color";
function tf(){
  var arr=str.split("-");
  for(var i=1;i<arr.length;i++){
    arr[i]=arr[i].charAt(0).toUpperCase()+arr[i].substring(1);
  }
  return arr.join("");
};
tf(str);

方法二:正则
var str="border-bottom-color";
function tf(){
  var re=/-(\w)/g;
  str=str.replace(re,function($0,$1){
    return $1.toUpperCase();
  });
  alert(str)
};
tf(str);

十四、图片懒加载

可以给img标签统一自定义属性data-src='default.png',当检测到图片出现在窗口之后再补充src属性,此时才会进行图片资源加载。

function lazyload() {
  const imgs = document.getElementsByTagName('img');
  const len = imgs.length;
  // 视口的高度
  const viewHeight = document.documentElement.clientHeight;
  // 滚动条高度
  const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop;
  for (let i = 0; i < len; i++) {
    const offsetHeight = imgs[i].offsetTop;
    if (offsetHeight < viewHeight + scrollHeight) {
      const src = imgs[i].dataset.src;
      imgs[i].src = src;
    }
  }
}

// 可以使用节流优化一下
window.addEventListener('scroll', lazyload);

十五、滚动加载

原理就是监听页面滚动事件,分析clientHeight、scrollTop、scrollHeight三者的属性关系。

window.addEventListener('scroll', function() {
  const clientHeight = document.documentElement.clientHeight;
  const scrollTop = document.documentElement.scrollTop;
  const scrollHeight = document.documentElement.scrollHeight;
  if (clientHeight + scrollTop >= scrollHeight) {
    // 检测到滚动至页面底部,进行后续操作
    // ...
  }
}, false);