手写源码系列

15,509 阅读11分钟

如今前端攻城狮的要求越来越高,会使用常见的API已经不能满足现如今前端日益快速发展的脚步。现在大厂基本都会要求面试者手写前端常见API的原理,以此来证明你对该知识点的理解程度。接下来,我将列举我面试时以及自认为比较重要的CSS部分、JS部分常见手写原理题!后续分享Vue全家桶、React全家桶手写原理图,敬请期待!

JS部分

手写原生Ajax

平时项目开发中,一定会用到ajax请求后端接口获取数据在前端渲染效果。目前市面上有很多封装好的ajax请求库,例如:jQuery版的ajax、基于Promise的Axios请求库、Flyio等等。目前很多人都只会使用这些API,但是如今面试经常会被问到:你了解ajax原理吗?你知道原生ajax实现步骤吗?...接下来,我将封装一个完整的原生ajax。

一个完整的 ajax 请求一般包括以下步骤:

  • 实例化 XMLHttpRequest 对象
  • 连接服务器
  • 发送请求
  • 介绍
function ajax(options) {
  let method = options.method || 'GET', // 不传则默认为GET请求
      params = options.params, // GET请求携带的参数
      data   = options.data, // POST请求传递的参数
      url    = options.url + (params ? '?' + Object.keys(params).map(key => key + '=' + params[key]).join('&') : ''),
      async  = options.async === false ? false : true,
      success = options.success,
      headers = options.headers;

  let xhr;
  // 创建xhr对象
  if(window.XMLHttpRequest) {
    xhr = new XMLHttpRequest();
  } else {
    xhr = new ActiveXObject('Microsoft.XMLHTTP');
  }

  xhr.onreadystatechange = function() {
    if(xhr.readyState === 4 && xhr.status === 200) {
      success && success(xhr.responseText);
    }
  }

  xhr.open(method, url, async);
  
  if(headers) {
    Object.keys(Headers).forEach(key => xhr.setRequestHeader(key, headers[key]))
  }

  method === 'GET' ? xhr.send() : xhr.send(data)
}

注意:IE5、6不兼容XMLHttpRequest,所以要使用ActiveXObject()对象,并传入 'Microsoft.XMLHTTP',达到兼容目的。

  • readyState的五种状态详解:

    0 - (未初始化)还没有调用send()方法

    1 - (载入)已调用send()方法,正在发送请求

    2 - (载入完成)send()方法执行完成,已经接收到全部响应内容

    3 - (交互)正在解析响应内容

    4 - (完成)响应内容解析完成,可以在客户端调用了

手写防抖和节流

如今前端界面效果越来越复杂,有一些频繁操作会导致页面性能和用户体验度低。像:输入框搜索会频繁调端口接口、放大缩小窗口等。

防抖 - debounce 当持续触发事件时,一定时间段内没有再触发事件,事件处理函数才会执行一次,如果设定的时间到来之前,又一次触发了事件,就重新开始延时。

const debounce = (fn, delay) => {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, delay);
  };
};

节流 - throttle 当持续触发事件时,保证一定时间段内只调用一次事件处理函数。

const throttle = (fn, delay = 500) => {
  let flag = true;
  return (...args) => {
    if (!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this, args);
      flag = true;
    }, delay);
  };
};

手写jsonp的实现原理

function jsonp({url, params, cb}) { 
   return new Promise((resolve, reject) => {
     window[cb] = function (data) {  // 声明全局变量
        resolve(data)
        document.body.removeChild(script)
      }
      params = {...params, cb}
      let arrs = []
      for(let key in params) {
         arrs.push(`${key}=${params[key]}`)
      }
      let script = document.createElement('script')
      script.src = `${url}?${arrs.join('&')}`
      document.body.appendChild(script)
   })
}

jsonp的缺点

  1. 只能发送Get请求 不支持post put delete
  2. 不安全 xss攻击

手写apply的实现原理

apply 的实现原理和 call 的实现原理差不多,只是参数形式不一样。--- 数组

Function.prototype.apply = function(content = window) {
    content.fn = this;
    let result;
    // 判断是否有第二个参数
    if(arguments[1]) {
        result = content.fn(...arguments[1]);
    } else {
        result = content.fn();
    }
    delete content.fn;
    return result;
}

注意:当apply传入的第一个参数为null时,函数体内的this会指向window。

手写bind的实现原理

bind 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。

Function.prototype.bind = function(content) {
   if(typeof this != 'function') {
      throw Error('not a function');
   }
   let _this = this;
   let args = [...arguments].slice(1);
   return function F() {
      // 判断是否被当做构造函数使用
      if(this instanceof F) {
         return _this.apply(this, args.concat([...arguments]))
      }
      return _this.apply(content, args.concat([...arguments]))
   }
}

手写call的实现原理

call语法:fun.call(thisArg, arg1, arg2, arg3, .....)

call 的核心原理:

  • 将函数设为对象的属性
  • 执行和删除这个函数
  • 指定this到函数并传入给定参数执行函数
  • 如果不传参数,默认指向window
Function.prototype.call2 = function(content = window) {
    // 判断是否是underfine和null
    // if(typeof content === 'undefined' || typeof content === null){
    //     content = window
    // }
    content.fn = this;
    let args = [...arguments].slice(1);
    let result = content.fn(...args);
    delete content.fn;
    return result;
}

注意:当call传入的第一个参数为null时,函数体内的this会指向window。

手写new的实现原理

实现一个new操作符的具体实现步骤:

  • 首先函数接受不定量的参数,第一个参数为构造函数,接下来的参数被构造函数使用
  • 然后内部创建一个空对象 obj
  • 因为 obj 对象需要访问到构造函数原型链上的属性,所以我们通过 setPrototypeOf 将两者联系起来。这段代码等同于 obj.proto = Con.prototype
  • 将 obj 绑定到构造函数上,并且传入剩余的参数
  • 判断构造函数返回值是否为对象,如果为对象就使用构造函数返回的值,否则使用 obj,这样就实现了忽略构造函数返回的原始值
/**
 * 创建一个new操作符
 * @param {*} Con 构造函数
 * @param  {...any} args 忘构造函数中传的参数
 */
  function createNew(Con, ...args) {
    let obj = {} // 创建一个对象,因为new操作符会返回一个对象
    Object.setPrototypeOf(obj, Con.prototype) // 将对象与构造函数原型链接起来
    // obj.__proto__ = Con.prototype // 等价于上面的写法
    let result = Con.apply(obj, args) // 将构造函数中的this指向这个对象,并传递参数
    return result instanceof Object ? result : obj
}

注意

一、new操作符的几个作用:

  1. new操作符返回一个对象,所以我们需要在内部创建一个对象
  2. 这个对象,也就是构造函数中的this,可以访问到挂载在this上的任意属性
  3. 这个对象可以访问到构造函数原型链上的属性,所以需要将对象与构造函数链接起来
  4. 返回原始值需要忽略,返回对象需要正常处理

二、new操作符的特点:

  1. new通过构造函数Test创建处理的实例可以访问构造函数中的属性也可以访问构造函数原型链上的属性,所以:通过new操作符,实例与构造函数通过原型链连接了起来
  2. 构造函数如果返回原始值,那么这个返回值毫无意义
  3. 构造函数如果返回对象,那么这个返回值会被正常的使用,导致new操作符没有作用

手写instanceof的实现原理

instanceof 用来检测一个对象在其原型链中是否存在一个构造函数的 prototype 属性

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__;
    }
}

手写Promise A+规范

在面试中高级前端时。要求被手写Promise A+规范源码是必考题了。如果想详细了解,请参考 一步步教你实现Promise/A+ 规范 完整版

class Promise {
    constructor(executor) {
        this.status = 'pending' // 初始化状态
        this.value = undefined // 初始化成功返回的值
        this.reason = undefined // 初始化失败返回的原因

        // 解决处理异步的resolve
        this.onResolvedCallbacks = [] // 存放所有成功的resolve
        this.onRejectedCallbacks = [] // 存放所有失败的reject

        /**
         * @param {*} value 成功返回值
         * 定义resolve方法
         * 注意:状态只能从pending->fulfilled和pending->rejected两个
         */
        const resolve = (value) => { 
            if(this.status === 'pending') {
                this.status = 'fulfilled' // 成功时将状态转换为成功态fulfilled
                this.value = value // 将成功返回的值赋值给promise
                // 为了解决异步resolve以及返回多层promise
                this.onResolvedCallbacks.forEach(fn => {
                    fn() // 当状态变为成功态依次执行所有的resolve函数
                })
            }
        }
        const reject = (reason) => {
            if(this.status === 'pending') {
                this.status = 'rejected' // 失败时将状态转换为成功态失败态rejected
                this.reason = reason // 将失败返回的原因赋值给promise
                this.onRejectedCallbacks.forEach(fn => {
                    fn() // 当状态变为失败态依次执行所有的reject函数
                })
            }
        }
        executor(resolve, reject) // 执行promise传的回调函数
    }
    /**
     * 定义promise的then方法 
     * @param {*} onFulfilled 成功的回调
     * @param {*} onRejected 失败的回调
     */
    then(onFulfilled, onRejected) {
        // 为了解决then方法返回Promise的情况
        const promise2 = new Promise((resolve, reject) => {
            if(this.status === 'fulfilled') { // 如果状态为fulfilled时则将值传给这个成功的回调
                setTimeout(() => {
                    const x = onFulfilled(this.value) // x的值有可能为 promise || 123 || '123'...
                    // 注意:此时调用promise2时还没有返回值,要用setTimeout模拟进入第二次事件循环;先有鸡先有蛋
                    resolvePromise(promise2, x, resolve, reject) 
                }, 0)
            }
            if(this.status === 'rejected') {
                setTimeout(() => {
                    const x = onRejected(this.reason) // 如果状态为rejected时则将视频的原因传给失败的回调
                    resolvePromise(promise2, x, resolve, reject) 
                }, 0)
            }
            if(this.status === 'pending') { // 记录-》解决异步
                this.onResolvedCallbacks.push(() => {
                    setTimeout(() => {
                        const x = onFulfilled(this.value)
                        resolvePromise(promise2, x, resolve, reject) 
                    }, 0)
                })
                this.onRejectedCallbacks.push(() => {
                    setTimeout(() => {
                        const x = onRejected(this.reason)
                        resolvePromise(promise2, x, resolve, reject) 
                    }, 0)
                })
            }
        })
        return promise2; // 解决多次链式调用的问题
    }
}

const resolvePromise = (promise2, x, resolve, reject) => {
    // console.log(promise2, x, resolve, reject)
    if(promise2 === x) { // 如果返回的值与then方法返回的值相同时
        throw TypeError('循环引用')
    }
    // 判断x是不是promise;注意:null的typeof也是object要排除
    if(typeof x === 'function' || (typeof x === 'object' && x !== null)) {
        try {
            const then = x.then // 获取返回值x上的then方法;注意方法会报错要捕获异常;原因111
            if(typeof then === 'function') { // 就认为是promise
                then.call(x, y => {
                    // resolve(y)
                    // 递归解析 ; 有可能返回多个嵌套的promise
                    resolvePromise(promise2, y, resolve, reject)
                }, r => {
                    reject(r)
                })
            }
        } catch(e) {
            reject(e)
        }
    } else {
        resolve(x);
    }
}
module.exports = Promise;

Symbol的用法以及常见应用

// Symbol 基本数据类型 string number boolean null undefined
// Symbol对象数据类型 object

// 特点:独一无二 永远不相等
let s1 = Symbol('tmc'); // symbol中的标识 一般只放number或string 不然结果返回Symbol([object Object])
let s2 = Symbol();
console.log(s1 === s2)

let obj = {
    [s1]: 1,
    a: 2
}
// 声明的Symbol属性是不可枚举的 for - in 可以遍历自身属性和原型上的属性ß
for(let key in obj) {
    console.log(obj[key])
}
// 获取对象上的属性
console.log(Object.getOwnPropertySymbols(obj));

let s3 = Symbol.for('tmc');
let s4 = Symbol.for('tmc');
console.log(s3 === s4);
// 通过Symbol来获取key值
console.log(Symbol.keyFor(s3))

// Symbol 内置对象
// Symbol.iterator 实现对象的遍历
// 元编程 可以去对原生js的操作进行修改
let instance = {
    [Symbol.hasInstance](value) {
        return 'a' in value;
    }
};
console.log({a: 3} instanceof instance)

let arr = [1, 2, 3];
arr[Symbol.isConcatSpreadable] = false; // 拼接数组时不展开
console.log([].concat(arr, [1, 2, 3]));

// match split search方法
let obj1 = {
    [Symbol.match](value) {
        return value.length === 3;
    }
}
console.log('abc'.match(obj1));

//species 衍生对象
class MyArray extends Array {
    constructor(...args) {
        super(...args)
    }
    // 强制修改一下
    static get [Symbol.species]() {
        return Array
    }
}
let v = new MyArray(1, 2, 3);
let c = v.map(item => item*=2); // c是v的衍生对象
console.log(c instanceof MyArray)

// Symbol.toPrimitive
// 数据类型转化
let obj3 = {
    [Symbol.toPrimitive](type) {
        console.log(type)
        return 123
    }
}
console.log(obj++)

// Symbol.toStringTag
let obj5 = {
    [Symbol.toStringTag]: 'xxxx'
}

console.log(Object.prototype.toString.call(obj5));

函数柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术

 // 通用版
function curry(fn, args) {
    var length = fn.length;
    var args = args || [];
    return function() {
        newArgs = args.concat(Array.prototype.slice.call(arguments))
        console.log(newArgs)
        if(newArgs.length < length) {
            return curry.call(this, fn, newArgs);
        } else {
            return fn.apply(this, newArgs);
        }
    }
}
function multiFn(a, b, c) {
    console.log(a * b * c)
    return a * b * c;
}

var multi = curry(multiFn);
multi(2)(3)(4);
// multi(2, 3, 4)
// multi(2)(3, 4)
// multi(2, 3)(4)

注意函数柯里化的主要作用和特点就是参数复用、提前返回和延迟执行。

JS数组

去重

普通项

let arr2 = [1, 2, 3, 2, 33, 55, 66, 3, 55];

第一种:

let newArr = [];
   arr2.forEach(item => {
       if(newArr.indexOf(item) == '-1') {
           newArr.push(item);
       }
   })
console.log(newArr);

// (6) [1, 2, 3, 33, 55, 66]

第二种:

let newArr = [...new Set(arr2)];
console.log(newArr);

// (6) [1, 2, 3, 33, 55, 66]

注意:Array.from()、filter()、for()等方法都可以完成上面数组去重。

对象项

let arr1 = [
    {id: 1, name: '汤小梦'},
    {id: 2, name: '石小明'},
    {id: 3, name: '前端开发'},
    {id: 1, name: 'web前端'}
];

实现方法:

const unique = (arr, key) => {
    return [...new Map(arr.map(item => [item[key], item])).values()]
}
console.log(unique(arr1, 'id'));

// [
	{id: 1, name: "web前端"},
	{id: 2, name: "石小明"},
	{id: 3, name: "前端开发"}
]

合并

let arr3 = ['a', 'b']
let arr4 = ['c', 'd']

方法一:ES5

let arr5 = arr3.concat(arr4);
console.log(arr5);

// ['a', 'b', 'c', 'd']

方法一:ES6

let arr6 = [...arr3, ...arr4];
console.log(arr6);

// ['a', 'b', 'c', 'd']

展平

let arr7 = [1, 2, [3, 4], [5, 6, [7, 8, 9]]];

第一种:

let arrNew = arr7.flat(Infinity);
console.log(arrNew);

// (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

第二种:

let arrNew = arr7.join().split(',').map(Number);
console.log(arrNew);

// (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

第三种:

let arrNew = arr7.toString().split(',').map(Number);
console.log(arrNew);

// (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

第四种:

const flattern = (arr) => {
     const result = []
     arr.forEach((item) => {
         if (Array.isArray(item)) {
              result.push(...flattern(item))
         } else {
              result.push(item)
         }
    })
    return result
}
flattern(arr7);

// (9) [1, 2, 3, 4, 5, 6, 7, 8, 9]

是否为数组

let arr = []

第一种:instanceof

console.log(arr instanceof Array)

第二种:constructor

console.log(arr.constructor === Array)

第三种:判断对象是否有 push 等数组的一些方法

console.log(!!arr.push && !!arr.concat)

第四种:toString

console.log(Object.prototype.toString.call(arr) === '[object Array]')

第五种:Array.isArray

console.log(Array.isArray(arr))

注意:第五种方式最优~

总结

上面总结的常见的CSS、JS部分手写原理题。希望有小伙伴需要的请认真思考阅读,必有收获。希望您取得满意的offer~❤️