JavaScript - 手写源码

112 阅读4分钟

1、Object.create(proto, defineProperties)

//利用工厂函数返回一个对象
//Object.create(obj)
// 参数1:proto 为新对象的原型指向
// 参数2:defineProperties 为新对象添加的自定义属性
Object.myCreate = function (proto, defineProperties) {
  // 实例化创建对象
  function F() {}
  F.prototype = proto;
  let obj = new F();

  // 添加自定义属性
  defineProperties && Object.defineProperties(obj, defineProperties);
  return obj;
};

let obj = { name: "张三", age: 18 };
let createObj = Object.myCreate(obj, {
  foo: {
    writable: true,
    configurable: true,
    value: "hello",
  },
});
console.log(createObj);

2、call & apply & bind

2.1 call

利用 函数执行上下文 原理 : that.myFn(...arg)

Function.prototype.newCall = function (that, ...arg) {
  that = that || window;
  that.myFn = this;
  const result = that.myFn(...arg); // es6语法
  delete that.myFn;
  return result;
};

const obj = {
  name: "joy",
};
function getName(a, b) {
  console.log(this.name, a, b);
}
getName.newApply(obj, 1, 2); // joy 1 2

纯Es5语法

Function.prototype.myCall = function (context) {
  //生成独一无二的id
  function mySymbol(obj) {
    let unique = Math.random() + new Date();
    if (obj.hasOwnProperty(unique)) {
      return mySymbol(obj);
    } else {
      return unique;
    }
  }

  // 生成唯一id
  let uniqueName = mySymbol(context);
  // 获取参数
  let args = [];
  for (let i = 1; i < arguments.length; i++) {
    // 使用逗号包裹,避免eval执行时;作为变量
    args.push("arguments[" + i + "]");
  }

  // 给传入的对象,绑定函数
  context[uniqueName] = this;

  //执行函数
  //1、如果直接 context[uniqueName]( args.join(",") )
  //   只当做一个字符串处理,不是多个参数
  //2、使用eval
  //   原理:eval直接执行,js字符串
  let result = eval("context[uniqueName](" + args.join(",") + ")");

  //删除临时方法
  delete context[uniqueName];

  //返回处理后的结果
  return result;
};

var obj1 = {
  getName: function (name) {
    return `${this.age},${name}`;
  },
};

var obj = { age: 18 };
var detail = obj1.getName.myCall(obj, "张三");
console.log(detail);

2.2 apply
Function.prototype.newApply = function (that, arg) {
  if (typeof this !== "function") {
    throw this + "is not a function";
  }
  that = that || window; // 因为第一个参数如果不传就会绑定到window上面
  arg = arg || [];
  that.myFn = this;
  const result = that.myFn(...arg); //解构赋值把参数传进来,先把结果存起来
  delete that.myFn; //再删除,否则就有副作用了
  return result;
};

const obj = {
  name: "joy",
};
function getName(a, b) {
  console.log(this.name, a, b);
}
getName.newApply(obj, [1, 2]); // joy

纯Es5语法

Function.prototype.myApply = function (context, args) {
  function mySymbol(obj) {
    let unique = Math.random() + new Date();
    if (obj.hasOwnProperty(unique)) {
      return mySymbol(obj);
    } else {
      return unique;
    }
  }

  let uniqueName = mySymbol(context);
  let arr = [];
  for (let i = 0; i < args.length; i++) {
    arr.push("args[" + i + "]");
  }

  context[uniqueName] = this;

  let result = eval("context[uniqueName](" + arr.join(",") + ")");

  delete context[uniqueName];

  return result;
};

var obj1 = {
  getName: function (name) {
    return `${this.age},${name}`;
  },
};

var obj = { age: 18 };
var detail = obj1.getName.myApply(obj, ["张三"]);
console.log(detail);

2.3 bind
Function.prototype.myBind = function (ctx) {
  var originFn = this,
    args = [].slice.call(arguments, 1),
    _tempFn = function () {};

  // 继承实例上的属性和方法
  var newFn = function () {
    var newArgs = [].slice.call(arguments);
    var newCtx = this instanceof newFn ? this : ctx;
    return originFn.apply(newCtx, args.concat(newArgs));
  };

  // 新的实例对象获取Foo上的原型
  _tempFn.prototype = this.prototype;
  newFn.prototype = new _tempFn(); // 继承原型上的方法
  return newFn; // 最终返回一个函数
};

3、 new Fn()

new的过程做了哪些事

  • 调用这个函数,创建一个空对象( 函数对象 )
  • 获取参数:args
  • 让函数中的this指向这个新对象 ( 创建出来的对象绑定this )
  • 默认返回这个对象 ( 即:创建的实例就是return的对象 )
    • 默认返回this对象
    • return 1; // 如果手动添加返回不是对象,不影响 this
    • return fn; // 如果手动返回一个对象,响应 this
// **** 构造函数不能作为构造函数,没有自己的this ****
function Cat(name, color) {
  this.name = name;
  this.color = color;
  return null;
}
Cat.prototype.miao = function () {
  console.log("喵~!");
};

// factory 工厂函数
// constructor 为函数对象
function newFactory(constructor) {
  // 1、创建对象
  let obj = Object.create(constructor.prototype);

  // 2、获取参数:arguments是类数组,不能直接就是用 slice 方法
  let args = Array.from(arguments).slice(1);

  // 3、执行函数
  let result = constructor.apply(obj, args);

  // 4、返回结果
  return typeof result === "object" ? result : obj;
}

let cat = newFactory(Cat, "大毛", "橘色");
cat.miao();

4、节流 & 防抖

节流 - 间隔执行、完成当前才能执行下一次

function throttle(func, wait) {
  let timmer = null;
  return function () {
    if (!timmer) {
      timmer = setTimeout(() => {
        clearTimeout(timmer);
        timmer = null;
        func.apply(this, arguments);
      }, wait);
    }
  };
}

function throttle(func, wait) {
  var prev = 0;
  return function () {
    let now = Date.now();
    if (now - prev > wait) {
      func.apply(this, arguments);
      prev = now;
    }
  };
}

防抖 - 延迟执行、清除上一次、重新开计时

function debounce(func, wait) {
  let timmer = null;
  return function () {
    if (timmer) clearTimeout(timmer), timmer == null;
    timmer = setTimeout(() => {
      func.apply(this, arguments);
    }, wait);
  };
}

5、深拷贝

function deepCp(resource) {
  let target;
  if (typeof resource == "object") {
    target = Array.isArray(resource) ? [] : {};

    // for...in 遍历,数组和对象都可以
    for (let key in resource) {
      if (Object.prototype.hasOwnProperty(resource, key)) {
        if (typeof resource[key] == "object") {
          target[key] = deepCp(resource[key]);
        } else {
          target[key] = resource[key];
        }
      }
    }
  } else {
    target = resource;
  }
  return target;
}
deepCp([
  { a: "aaa", b: { bb: "bbb" } },
  { a: "aaa", b: { bb: "bbb" } },
]);

6、图片懒加载

// 获取当前页面全部的 image 标签
var oImg = document.getElementsByTagName("img");
fn();

window.onscroll = function () {
  fn(); // 滚轮滚动事件( 可以使用防抖技术 )
};

function fn() {
  // 判断图片是否在可视区内
  for (var i = 0; i < oImg.length; i++) {
    // 当前元素顶部距离最近父元素顶部的距离( 默认指向body )
    //offsetTop:,除非自己制定父元素为relative、absolute
    var oImgTo = oImg[i].offsetTop;

    // 网页可见区域高
    var clientH = document.documentElement.clientHeight;

    // 滚动条的垂直距离( 向上滚动隐藏的高度 )
    var scrollT = document.documentElement.scrollTop || document.body.scrollTop;

    // 可视区顶部距离页面顶部的距离
    if (clientH + scrollT >= oImgTo && !oImg[i].isLoaded) {
      oImg[i].src = oImg[i].getAttribute("_src");
      oImg[i].setAttribute("isLoaded", true);
    }
  }
}

7、发布 & 订阅

// 事件触发器
class EventEmitter {
  constructor() {
    this.subs = {};
  }
  // 注册事件
  $on(eventType, handler) {
    this.subs[eventType] = this.subs[eventType] || [];
    this.subs[eventType].push(handler);
  }
  // 触发事件
  $emit(eventType) {
    if (this.subs[eventType]) {
      this.subs[eventType].forEach((handler) => {
        handler();
      });
    }
  }
}

// 测试
let em = new EventEmitter();
em.$on("click", () => {
  console.log("click1");
});

em.$on("click", () => {
  console.log("click2");
});

em.$emit("click");

8、观察者模式

image.png

  • 观察者模式

    • 由具体目标(发布者) 调度(Dep) ,订阅者更新(watcher )
    • 观察者模式的订阅者与发布者之间存在 依赖关系
  • 发布/订阅者模式

    • 包含事件中心( 统一调用中心 )
    • 去除发布者和订阅者的依赖( 组件A订阅事件,组件B中触发 )
// 发布者-目标 收集订阅者、通知订阅者更新
class Dep {
  constructor() {
    // 记录所有的订阅者
    this.subs = [];
  }
  // 添加订阅者
  addSub(sub) {
    if (sub && sub.update) {
      this.subs.push(sub);
    }
  }
  // 发布通知
  notify() {
    this.subs.forEach((sub) => {
      sub.update();
    });
  }
}

// 订阅者-观察者 被收集、
class Watcher {
  update() {
    console.log("update");
  }
}

// 测试
let dep = new Dep();
let watcher = new Watcher();
dep.addSub(watcher);
dep.notify();

9、控制并发量

function sendRequest(chunks, limit = 4) {
  return new Promise((resolve, reject) => {
    let isStop = false;
    let start = async function () {
      if (isStop) return;
      let work = chunks.shift();
      if (work) {
        let [form, index] = work;
        try {
          await axios.post("/update", form);
          if (chunks.length) start();
          else resolve();
        } catch (error) {
          if (error.count < 3) {
            error.count++;
            chunks.unshift(work);
            start();
          } else {
            isStop = true;
            reject();
          }
        }
      }
    };

    while (limit) {
      start();
      limit--;
    }
  });
}

10、实现单点登录

原理:统一登陆SSO,cookie共享(顶级域名)

  • 已被登陆
    • 初次访问当前项目,header中会被设置cookie
    • 请求时也会自动携带过去
    • 此时会验证通过
  • 未登录
    • 访问次项目,无cookie
    • 重定向到统一的登陆页面
    • 登陆成功后,自动设置cookie( doman:顶级域名, path:/)
    • 子域名会共享顶级域名的cookie
import axios from "axios";
import store from "@/store";
import { Message } from "element-ui";
import router from "@/router";
import qs from "qs";

const request = axios.create({
  baseURL: "/api",
});

function redirectLogin() {
  router.push({
    name: "login",
    query: {
      redirect: router.currentRoute.fullPath,
    },
  });
}

// refresh_token 只能使用1次
function refreshToken() {
  return axios.create()({
    method: "POST",
    url: "/front/user/refresh_token",
    data: qs.stringify({
      refreshtoken: store.state.user.refresh_token,
    }),
  });
}

// 请求拦截器 - 设置token
request.interceptors.request.use(
  function (config) {
    const { user } = store.state;
    if (user && user.access_token) {
      config.headers.Authorization = user.access_token;
    }
    return config;
  },
  function (error) {
    return Promise.reject(error);
  }
);

// 响应拦截器
let isRfreshing = false; // 控制刷新 token 的状态
let requests: Array<() => void> = []; // 存储刷新 token 期间过来的 401 请求

request.interceptors.response.use(
  function (response) {
    // 1、状态码为 2xx 都会进入这里
    // 如果是自定义错误状态码,错误处理就写到这里
    return response;
  },
  async function (error) {
    // 2、超出 2xx 状态码都都执行这里
    if (error.response) {
      const { status } = error.response;
      if (status === 400) {
        Message.error("请求参数错误");
      } else if (status === 401) {
        // token无效 或 过期
        // user 不存直接从新登录
        if (!store.state.user) {
          redirectLogin();
          return Promise.reject(error);
        } // 刷新 token :只能刷新一次

        if (!isRfreshing) {
          isRfreshing = true; // 开启刷新状态 // 尝试刷新获取新的 token
          return refreshToken()
            .then((res) => {
              if (!res.data.success) {
                throw new Error("刷新 Token 失败");
              } // 刷新 token 成功了
              store.commit("setUser", res.data.content); // 执行 requests 队列中的请求
              requests.forEach((cb) => cb()); // 重置 requests 数组
              requests = [];
              return request(error.config);
            })
            .catch(() => {
              Message.warning("登录已过期,请重新登录");
              store.commit("setUser", null);
              redirectLogin();
              return Promise.reject(error);
            })
            .finally(() => {
              isRfreshing = false; // 重置刷新状态
            });
        } // 1、refreshToken 的请求是异步的 // 2、页面调用接口时是同步的,此时会收集所有接口放入 requests 数组中 // 3、等token重新刷新后,依次重新执行requests中的接口 request(error.config)

        return new Promise((resolve) => {
          requests.push(() => {
            resolve(request(error.config));
          });
        });
      } else if (status === 403) {
        Message.error("没有权限,请联系管理员");
      } else if (status === 404) {
        Message.error("请求资源不存在");
      } else if (status >= 500) {
        Message.error("服务端错误,请联系管理员");
      }
    } else if (error.request) {
      // 请求发出去没有收到响应
      Message.error("请求超时,请刷新重试");
    } else {
      // 在设置请求时发生了一些事情,触发了一个错误
      Message.error(`请求失败:${error.message}`);
    } // 把请求失败的错误对象继续抛出,扔给上一个调用者

    return Promise.reject(error);
  }
);

export default request;