手写系列

针对原生 JavaScript 的一些实践练习。

JS 原生功能

new操作符

答案

可参考new 发生了什么?

function myNew(...args) {
  const Constructor = args[0];
  // 创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
  const obj = Object.create(Constructor.prototype);
  const params = args.slice(1);
  const res = Constructor.apply(obj, params);
  // 判断返回值是否是对象类型
  if (typeof res === "object") return res;
  return obj;
}

发布/订阅模式

答案
class EventBus {
  constructor() {
    this.events = Object.create(null);
  }
  on(event, fn) {
    this.events.event = this.events.event || [];
    this.events.event.push(fn);
  }
  off(event, fn) {
    const index = (this.events.event || []).indexOf(fn);
    if (index < -1) {
      return;
    } else {
      this.events.event.splice(index, 1);
    }
  }
  fire(event) {
    this.events.event.forEach((fn) => fn());
  }
}
var bus = new EventBus();
bus.on("onclick", function() {
  console.log("click1 fire");
});
bus.on(
  "onclick",
  (fn = function() {
    console.log("click2 fire");
  })
);
bus.fire("onclick");

观察者模式

实现
class Observer {
  constructor(fn) {
    this.update = fn;
  }
}
class Subject {
  constructor() {
    this.observers = [];
  }
  addObserver(observer) {
    this.observers.push(observer);
  }
  removeObserver(observer) {
    const delIndex = this.observers.indexOf(observer);
    this.observers.splice(delIndex, 1);
  }
  notify() {
    this.observers.forEach((observer) => {
      observer.update();
    });
  }
}

var subject = new Subject();
var ob1 = new Observer(function() {
  console.log("ob1 callback run");
});
subject.addObserver(ob1);
var ob2 = new Observer(function() {
  console.log("ob2 callback run");
});
subject.addObserver(ob2);
subject.notify();

应用:如 Vue中双向绑定。

Promise

实现
class Promise{
  constructor(executor){
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    let resolve = value => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onResolvedCallbacks.forEach(fn=>fn());
      }
    };
    let reject = reason => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        this.onRejectedCallbacks.forEach(fn=>fn());
      }
    };
    try{
      executor(resolve, reject);
    } catch (err) {
      reject(err);
    }
  }
  then(onFulfilled,onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err };
    let promise2 = new Promise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        setTimeout(() => {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
      if (this.state === 'rejected') {
        setTimeout(() => {
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      };
      if (this.state === 'pending') {
        this.onResolvedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0)
        });
      };
    });
    return promise2;
  }
  catch(fn){
    return this.then(null,fn);
  }
}
function resolvePromise(promise2, x, resolve, reject){
  if(x === promise2){
    return reject(new TypeError('Chaining cycle detected for promise'));
  }
  let called;
  if (x != null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      let then = x.then;
      if (typeof then === 'function') { 
        then.call(x, y => {
          if(called)return;
          called = true;
          resolvePromise(promise2, y, resolve, reject);
        }, err => {
          if(called)return;
          called = true;
          reject(err);
        })
      } else {
        resolve(x);
      }
    } catch (e) {
      if(called)return;
      called = true;
      reject(e); 
    }
  } else {
    resolve(x);
  }
}
//resolve方法
Promise.resolve = function(val){
  return new Promise((resolve,reject)=>{
    resolve(val)
  });
}
//reject方法
Promise.reject = function(val){
  return new Promise((resolve,reject)=>{
    reject(val)
  });
}
//race方法 
Promise.race = function(promises){
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(resolve,reject)
    };
  })
}
//all方法(获取所有的promise,都执行then,把结果放到数组,一起返回)
Promise.all = function(promises){
  let arr = [];
  let i = 0;
  function processData(index,data){
    arr[index] = data;
    i++;
    if(i == promises.length){
      resolve(arr);
    };
  };
  return new Promise((resolve,reject)=>{
    for(let i=0;i<promises.length;i++){
      promises[i].then(data=>{
        processData(i,data);
      },reject);
    };
  });
}

柯里化

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

柯里化有 3 个常见作用:

  • 参数复用
  • 提前返回
  • 延迟计算/运行

1. 求和函数

实现
// 普通方式
var add1 = function(a, b, c) {
  return a + b + c;
};
// 柯里化
var add2 = function(a) {
  return function(b) {
    return function(c) {
      return a + b + c;
    };
  };
};

这里每次传入参数都会返回一个新的函数,这样一直执行到最后一次返回 a+b+c 的值。 但是这种实现还是有问题的,这里只有三个参数,如果哪天产品经理告诉我们需要改成100次?我们就重新写100次?这很明显不符合开闭原则,所以我们需要对函数进行一次修改。

实现
const curry = (fun, initArgs) => {
  let _this = this;
  let len = fun.length; // 被改造函数参数的个数
  let args = initArgs || [];

  return function() {
    let _args = [...args, ...arguments]; // 参数

    // 如果参数个数小于最初的fun.length,则递归调用,继续收集参数
    if (_args.length < len) {
      return curry.call(_this, fun, _args);
    }

    // 参数收集完毕,则执行函数,返回结果
    return fun.apply(this, _args);
  };
};

const sum = curry(function(a, b, c) {
  return a + b + c;
});

console.log(sum(1, 2, 3));
console.log(sum(1, 2)(3));
console.log(sum(1)(2, 3));

应用方向

防抖

在事件被触发 n 事件后再执行回调函数,如果在这 n 秒内又被触发,则重新计时。

场景:

  • 输入框中连续输入一串字符后,只会在输入完后去执行最后一次的查询 ajax 请求,这样可以有效减少请求次数,节约请求资源
  • window 的 resizescroll 事件,不断地调整浏览器的窗口大小、或者滚动时会触发对应事件,防抖让其只触发一次
实现
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    let context = this;
    if (timer) clearTimeout(timer);
    timer = setTimeout(function() {
      fn.apply(context, args);
    }, delay);
  };
}

节流

规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。

场景:

  • 连续不断地触发某事件(如点击),只在单位时间内只触发一次
  • 在页面的无限加载场景下,需要用户在滚动页面时,每隔一段时间发一次 ajax 请求,而不是在用户停下滚动页面操作时才去请求数据
  • 监听滚动事件,比如是否滑到底部自动加载更多
实现
function throttle(fun, delay) {
  let flag = true,
    timer = null;
  return function(...args) {
    let context = this;
    if (!flag) return;
    flag = false;
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(context, args);
      flag = true;
    }, delay);
  };
}

总结下防抖和节流的区别:

-效果:

函数防抖是某一段时间内只执行一次;而函数节流是间隔时间执行,不管事件触发有多频繁,都会保证在规定时间内一定会执行一次真正的事件处理函数。

-原理:

防抖是维护一个计时器,规定在 delay 时间后触发函数,但是在 delay 时间内再次触发的话,都会清除当前的 timer 然后重新设置超时调用,即重新计时。这样一来,只有最后一次操作能被触发。

节流是通过判断是否到达一定时间来触发函数,若没到规定时间则使用计时器延后,而下一次事件则会重新设定计时器。

深浅拷贝

浅拷贝

如果我们要复制对象的所有属性都不是引用类型时,就可以使用浅拷贝,实现方式就是遍历并复制,最后返回新的对象。

实现
function shallowCopy(obj) {
  var copy = {};
  // 只复制可遍历的属性
  for (key in obj) {
    // 只复制本身拥有的属性
    if (obj.hasOwnProperty(key)) {
      copy[key] = obj[key];
    }
  }
  return copy;
}

我们使用浅拷贝会复制所有引用对象的指针,而不是具体的值,所以使用时一定要明确自己的需求,同时,浅拷贝的实现也是最简单的。

S 内部实现了浅拷贝,如 Object.assign(),其中第一个参数是我们最终复制的目标对象,后面的所有参数是我们的即将复制的源对象,支持对象或数组,一般调用的方式为:

var newObj = Object.assign({}, originObj);

深拷贝

简单的深拷贝:不处理循环引用,不处理对象原型,函数依然是引用类型

实现
var deepClone = function(currobj) {
  if (typeof currobj !== "object") {
    return currobj;
  }
  if (currobj instanceof Array) {
    var newobj = [];
  } else {
    var newobj = {};
  }
  for (var key in currobj) {
    if (typeof currobj[key] !== "object") {
      // 不是引用类型,则复制值
      newobj[key] = currobj[key];
    } else {
      // 引用类型,则递归遍历复制对象
      newobj[key] = deepClone(currobj[key]);
    }
  }
  return newobj;
};

另外还有一种方式是使用 JSON 序列化,巧妙但是限制更多:

// 调用JSON内置方法先序列化为字符串再解析还原成对象
newObj = JSON.parse(JSON.stringify(obj));

JSON 是一种表示结构化数据的格式,只支持简单值、对象和数组三种类型,不支持变量、函数或对象实例。所以我们工作中可以使用它解决常见问题,但也要注意其短板:函数会丢失,原型链会丢失,以及上面说到的所有缺陷。

完全实现
/**
 * 深拷贝关注点:
 * 1. JavaScript内置对象的复制: Set、Map、Date、Regex等
 * 2. 循环引用问题
 * @param {*} object
 * @returns
 */
function deepClone(source, memory) {
  const isPrimitive = (value) => {
    return /Number|Boolean|String|Null|Undefined|Symbol|Function/.test(
      Object.prototype.toString.call(value)
    );
  };
  let result = null;

  memory || (memory = new WeakMap());
  // 原始数据类型及函数
  if (isPrimitive(source)) {
    console.log("current copy is primitive", source);
    result = source;
  }
  // 数组
  else if (Array.isArray(source)) {
    result = source.map((value) => deepClone(value, memory));
  }
  // 内置对象Date、Regex
  else if (Object.prototype.toString.call(source) === "[object Date]") {
    result = new Date(source);
  } else if (Object.prototype.toString.call(source) === "[object Regex]") {
    result = new RegExp(source);
  }
  // 内置对象Set、Map
  else if (Object.prototype.toString.call(source) === "[object Set]") {
    result = new Set();
    for (const value of source) {
      result.add(deepClone(value, memory));
    }
  } else if (Object.prototype.toString.call(source) === "[object Map]") {
    result = new Map();
    for (const [key, value] of source.entries()) {
      result.set(key, deepClone(value, memory));
    }
  }
  // 引用类型
  else {
    if (memory.has(source)) {
      result = memory.get(source);
    } else {
      result = Object.create(null);
      memory.set(source, result);
      Object.keys(source).forEach((key) => {
        const value = source[key];
        result[key] = deepClone(value, memory);
      });
    }
  }
  return result;
}
上次更新:
贡献者: liuzhu