Reinventing the wheel

由造轮子引发的思考

MY NEIGHBOUR TOTORO
MY NEIGHBOUR TOTORO

JavaScript Basic


Object.XXX series

  • 手写 Object.create

new 和 Object.create 都用于创建对象,但前者执行构造函数,后者不执行,且使用现有的对象来提供新创建的对象的 __proto__。

// 声明一个类
function Human(){ alert("Human func"); }
Human.prototype.say=function(){ alert("Human func say hello world"); };
function Male(){}
Male.prototype=new Human(); // 继承Human
Male.prototype.constructor=Male;
Male.prototype.say=function(){ alert("Male func say hello world"); };
var m = new Male();
console.log(m instanceof Male); // true
console.log(m instanceof Human); // true
// ---------
function Human(){ alert("Human func"); }
Human.prototype.say=function(){ alert("Human func say hello world"); };
// 声明一个子类
function Male(){}
Male.prototype=Object.create(Human.prototype); // 继承Human
Male.prototype.constructor=Male;
Male.prototype.say=function(){ alert("Male func say hello world"); };
var m=new Male();
console.log(m instanceof Male); // true
console.log(m instanceof Human); // true
/* Object.create实现 */
// Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__.
// 思路 => 将传入的对象作为原型
function create(obj) {
  function F() {}
  F.prototype = obj
  return new F()
}
  • 冻结对象

Clue => vm
当一个 Vue 实例被创建时,会将 data 对象中的所有的 property 加入到 Vue 的响应式系统中。当这些 property 的值发生改变时,视图将会产生响应,即匹配更新为新的值。
当这些数据改变时,视图会进行重渲染。值得注意的是只有当实例被创建时就已经存在于 data 中的 property 才是响应式的。
如果知道会在晚些时候需要一个 property,但是一开始其为空或不存在,那么仅需要设置一些初始值。
这里唯一的例外是使用 Object.freeze(),这会阻止修改现有的 property,也意味着响应系统无法再追踪变化。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>VueTest</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
  </head>
  <body>
    <div id="app-8">
      <p>{{ foo }}</p>
      <!-- 这里的 `foo` 不会更新! -->
      <button v-on:click="foo = 'baz'">Change it</button>
    </div>
  </body>
  <script>
    var obj = {
      foo: 'bar'
    }
    Object.freeze(obj);
    var app8 = new Vue({
      el: '#app-8',
      data: obj
    })
  </script>
</html>

手写 new

new 运算符创建用户定义的对象类型的实例或具有构造函数的内置对象的实例。

  • 内存中创建新对象;
  • 新对象的隐式原型指向构造函数的显式原型 => 继承父类原型上的方法;
  • 构造函数中的 this 指向这个新对象 => 新对象初始化属性和方法;
  • 如果是返回的是值类型或者没有返回值 => 返回创建的对象;如果返回的是引用类型 => 返回这个引用类型对象。
function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判断参数是否是一个函数
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  newObject = Object.create(constructor.prototype);
  // 将 this 指向新建对象,并执行函数
  result = constructor.apply(newObject, arguments);
  // 判断返回对象
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判断返回结果
  return flag ? result : newObject;
}
function Person(name) {
  this.name = name;
}
Person.prototype.sayName = function(){
  console.log(`!! => ${this.name}`)
}
const me = objectFactory(Person,'zs');
me.sayName()
function myNew(func, ... args){
  const instance = {};
  if(func.prototype) Object.setPrototypeOf(instance, func.prototype);
  const res = func.apply(instance, args);
  if(typeof res == "function" || (typeof res === "object" && res !== null)) return res
  return instance;
}
function Person(name) {
  this.name = name;
}
Person.prototype.sayName = function(){
  console.log(`!! => ${this.name}`)
}
const me = myNew(Person,'zs');
me.sayName()

手写 instanceof

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

// 获取类型的原型 => 获得构造函数与实例的原型 => 循环判断实例的原型是否等于构造函数的原型 => 不等于往上找,直到对象原型为 null,因为原型链最终为 null.
function myInstanceof(arg1, arg2) {
  // 获取实例的原型以及构造函数的原型
  // Object.getPrototypeOf() 方法返回指定对象的原型
  let pro = Object.getPrototypeOf(arg1),prot = arg2.prototype;
  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    // pro非null
    if (!pro) return false;
    if (pro === prot) return true;
    pro = Object.getPrototypeOf(pro);
  }
}
// test
function Car(make, model, year) {
  this.make = make;
  this.model = model;
  this.year = year;
}
const auto = new Car('Honda', 'Accord', 1998);
console.log(myInstanceof(auto,Car)); // true

debounce & throttle

  • 防抖 debounce => 一段时间只触发最后一次

事件触发时创建定时器延迟;事件再次触发则清除并重置定时器。常应用于输入框防抖,以节约性能。

function debounce (func, ms = 1000) {
  let timer; // 产生独立的作用域
  return function (...args) { // 返回函数到调用位置 => 事件触发就会执行返回的函数
    if (timer) { clearTimeout(timer) } // 内部函数使用外部函数的 timer 形成闭包
    timer = setTimeout(() =>{ func.apply(this, args) }, ms) // this 指向函数调用者 => 箭头函数的 this 指向外层函数作用域中 this 的值
  }
}
const test = () => { console.log('test', this) } // test Window{}
const debouncetest = debounce(test, 1000)
window.addEventListener ('mouseover', debouncetest)
  • 节流 throttle => 一段时间只触发一次

事件触发时判断有无定时器,有则返回;定时器执行完后重置。常应用于按钮连续点击所造成的事件频繁触发,以避免卡顿。

function throttle(func, ms = 1000) {
  let timer
  return function (...args) {
    if (timer) return
    timer = true
    setTimeout(() => { func.apply(this, args); timer = null}, ms)
  }
}
const test = () => { console.log('test', this) } // test Window{}
const throttletest = throttle(test, 1000)
window. addEventListener('mouseover', throttletest)

stack & heap & closure

栈在内存中是用于连续存储局部变量和函数参数的线性结构。严格先进后出,且入栈和出栈的操作仅是栈指针在内存地址中的上下移动。栈在函数调用时创建,调用结束则消失。栈中仅存对数据的引用,也就是该块数据的首地址。

function Closure () {
  let num = 0;
  return function () {
    num++;
    return num;
  }
}
let Closuree = Closure();
Closuree(); // 1
Closuree(); // 2

通常调用完函数会销毁栈,不应该存在后续变量值的改变。实际上闭包所访问的变量并没有存储在栈中,而是在堆内存里,用一个特殊的对象 [[Scopes]] 保存。

// console.dir() 可以显示一个对象所有的属性和方法
// console.dir(Closuree)
ƒ anonymous()
arguments: null
caller: null
length: 0
name: ""
prototype: {constructor: ƒ}
[[FunctionLocation]]: VM2400:3
[[Prototype]]: ƒ ()
[[Scopes]]: Scopes[3]
0: Closure (Closure) {num: 2}
1: Script {Closuree: ƒ}
2: Global {0: Window, 1: Window,...}

[[Scopes]] 是 Chrome 开发者工具在内部添加和使用的私有属性,表示函数范围内的变量,或者说可以从该函数访问哪些变量。

局部作用域对应局部变量,全局作用域对应全局变量,而被捕获变量是在函数中声明,却在函数返回后仍被未执行作用域(函数或是类)持有的变量(闭包返回的变量)。

deep and shallow copy

引用类型的值实际存储在堆内存中,栈只存储堆内地址的引用,而这个地址的引用指向堆内存中的值。

浅拷贝仅拷贝一层,对更深层次对象级别只拷贝引用,在操作数据时难免出现对象互相影响的情况;深拷贝会逐层拷贝所有属性,地址也与原来的不同,改变属性时也就不会影响原来的数据。

  • 深拷贝 deep copy => lodash、JSON.parse、make wheels
// lodash
const _ = require('lodash');
const objTest = [ { a: 1, b: 2 }, { a: 1, b: 2 } ]
const resObject = _.cloneDeep(objTest)
console.log('相等比较 ==>', objTest === resObject); // 相等比较 ==> false
// JSON.parse() 和 JSON.stringify() 组合 => 只限于可被 JSON.stringify() 编码的值 -> Boolean, Number, String, Object, Array
const objTest = [ { a: 1, b: 2 }, { a: 1, b: 2 } ]
const resObject = JSON.parse(JSON.stringify(objTest));
console.log('相等比较 ==>', objTest === resObject); // 相等比较 ==> false
// make wheels
function deepCopyFunc(inputObject, cache = new WeakMap()){
  if (typeof inputObject !== 'object' || inputObject === null) return inputObject;
  if(inputObject instanceof Date) return new Date(inputObject)
  if(inputObject instanceof RegExp) return new RegExp(inputObject)
  if(cache.get(inputObject)) return cache.get(inputObject) // 循环引用则返回缓存的对象防止递归死循环
  let outputObject = new inputObject.constructor();
  cache.set(inputObject, outputObject);
  for(const [key, value] of Object.entries(inputObject)){
    outputObject[key] = deepCopyFunc(value);
  }
  return outputObject;
}
// Test
const objTest = { a:1, b:'ok', c:{ code:'js' }, d:['java','cpp'], e: new Date(), f: /^ok$/ }
const resObject = deepCopyFunc(objTest);
resObject.c.code = "ts";
console.log(resObject == objTest);
console.log(resObject);
Array Methods => Array.from()、Array.prototype.slice()
// make wheels
function shallowCopyFunc(inputObject) {
  let resObject = {};
  for (const key in inputObject) {
    resObject[key] = inputObject[key];
  }
  return resObject;
}

Flatten & Currying

  • 数组扁平化 Flatten an Array => 将多维数组转化为一维数组

toString & split 和 join & split => 数组-字符串-数组;需要注意 split 形成的数组中每个元素仍然是字符串,需要将其转化为数字。

const arr = [1, [2, 3, [4, 5]], 6]
function flatten(arr) {
  return arr.toString().split(',').map(item => Number(item))
}
console.log(flatten(arr)) // [1, 2, 3, 4, 5]
const arr = [1, [2, 3, [4, 5]], 6]
function flatten(arr) {
  return arr.join(',').split(',').map( item => parseInt(item) )
}
console.log(flatten(arr)) // [1, 2, 3, 4, 5]

reduce & 递归 & 扩展运算符 => 对数组的每个元素进行操作

let arr = [1, [2, 3, [4, 5]], 6]
const newArr = function(arr){
  return arr.reduce((pre,cur)=>pre.concat(Array.isArray(cur)?newArr(cur):cur),[])
}
console.log(newArr(arr)); //[1, 2, 3, 4, 5, 6]
function flatten(arr) {
  var res = [];
  arr.map(item => {
    if(Array.isArray(item)) {
      res = res.concat(flatten(item));
    } else { res.push(item); }
  });
  return res;
}
function flatten(arr) {
  while (arr.some(item => Array.isArray(item))) {
    arr = [].concat(...arr);
  }
  return arr;
}
let arr = [1, [2, 3, [4, 5]]] 
console.log(flatten(arr)); // [1, 2, 3, 4, 5]
  • Array.prototype.flat([depth]) => 按照指定的深度递归遍历数组;深拷贝
// 使用 Infinity 作为深度,展开任意深度的嵌套数组
let arrTest01 = [1, 2, [3, 4, [5, 6]]]
arrTest01.flat(Infinity); // [1, 2, 3, 4, 5, 6]
// 移除数组中的空项
let arrTest02 = [1, 2, , 4, 5];
arrTest02.flat(); // [1, 2, 4, 5]
  • 对象扁平化 Flatten Object => 将复杂的多层的对象转化为一维对象
function toFlatPropertyMap(obj, keySeparator = '.') {
  const flattenRecursive = (obj, parentProperty, propertyMap = {}) => {
    for(const [key, value] of Object.entries(obj)){
      const property = parentProperty ? `${parentProperty}${keySeparator}${key}` : key;
      if(value && typeof value === 'object'){
        flattenRecursive(value, property, propertyMap);
      } else {
        propertyMap[property] = value;
      }
    }
    return propertyMap;
  };
  return flattenRecursive(obj);
}
let testObj = { a: { b: { c: 1, d: 2 }, e: 3 }, f: { g: 2 }, h: [1, 2] }
console.log(toFlatPropertyMap(testObj))

Typescript Version => Record<Keys, Type> => SOF

function toFlatPropertyMap(obj: object, keySeparator = '.') {
  const flattenRecursive = (obj: object, parentProperty?: string, propertyMap: Record<string, unknown> = {}) => {
    for(const [key, value] of Object.entries(obj)){
      const property = parentProperty ? `${parentProperty}${keySeparator}${key}` : key;
      if(value && typeof value === 'object'){
        flattenRecursive(value, property, propertyMap);
      } else {
        propertyMap[property] = value;
      }
    }
    return propertyMap;
  };
  return flattenRecursive(obj);
}
  • 柯里化 Currying

函数的柯里化是将使用多个参数的函数转换成一系列使用一个参数的函数。已被柯里化的函数在执行后不能返回一个值,只能返回一个函数;调用完毕时要能在脚本引擎中解析得到结果;因为在连续调用时要保留前置传入的参数,所以要考虑通过闭包不让参数销毁;不限制的调用次数涉及到递归。

function curry(){
  var args = Array.prototype.slice.call(arguments); // 不做阈值条件增加复用性
  const appendFunc = function(){ // 声明追加函数
    args.push(...arguments); // 追加函数调用传参放入前置传参的数组 => 实现闭包
    return appendFunc; // 连续调用也保持返回函数
  }
  appendFunc.toString = function(){
    return args.reduce((prev, next) => prev + next, 0);
  }
  return appendFunc;
}
console.log(curry(1)(2) + '')
function curry(fn) {
  const presetArgs = [].slice.call(arguments, 1);
  function appendFunc() {
    const restArgs = [].slice.call(arguments);
    const allArgs = [...presetArgs, ...restArgs];
    return curry.call(null, fn, ...allArgs);
  }
  appendFunc.toString = function () {
    return fn.apply(null, presetArgs);
  };
  return appendFunc;
}
function sumFunc() {return Array.from(arguments).reduce((prev, curr) => { return prev + curr; }, 0)};
var res = curry(sumFunc);
console.log(res(1)(2)(3)(4) + ""); // 10
console.log(res(1, 2)(3, 4)(5, 6) + ""); // 21
  • 斐波那契数列 Fibonacci => 递归

在数学上,斐波那契数是以递归的方法来定义:F0=0 F1=1 ... Fn=Fn-1 + Fn-2,用文字来说,就是斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

// 斐波那契数列: 0 1 1 2 3 5 8 13 ...(0不是第一项,而是第0项)
function fibonacci(n) {
  if(n==0 || n == 1)
    return n;
  return fibonacci(n-1) + fibonacci(n-2);
}

Comprehension

Null 与 Undefined

null 转为数值时以 0 表示,常作为"无"的对象。undefined 转为数值时为 NaN,常表示"无"的原始值。其次 undefined 不是一个有效的 JSON,而 null 是。

null 作为一个字面量,常指对象的值未设置。多见于对象原型链的终点,或是作为函数的参数。

undefined 是全局对象的一个属性,即是全局作用域的一个变量。如果方法没有返回值或是语句中操作的变量没有被赋值,那么默认返回 undefined。

if (!undefined) 
    console.log('undefined is false');
// undefined is false

if (!null) 
    console.log('null is false');
// null is false

undefined == null
// 抽象(非严格)相等比较 (==) true

undefined === null
// 严格相等比较 (===) false
//null->对象原型链的终点
Object.prototype.__proto__ === null

//undefined->调用函数时,应该提供的参数没有提供,该参数等于undefined
var a="zs"
function hrb(){
    console.log(a)
    a="xc"
}
hrb() // undefined

Utils

异步控制并发数

function limitOfAsyncConcurrentRequests(){

}

获取 URL 参数

URLSearchParams 接口定义了一些实用的方法来处理 URL 的查询字符串。一个实现了 URLSearchParams 的对象可以直接用在 for...of 结构中。

URLSearchParams 构造函数不会解析完整 URL,但是如果字符串起始位置有 ? 的话会被去除。

const urlSearchParams = new URLSearchParams(window.location.search);
const params = Object.fromEntries(urlSearchParams.entries());
function getUrlParams(url){
  const res = {};
  if(url.includes('?')){
    const str = url.split('?')[1], arr = str.split('&');
    arr.forEach(item => {
      const key = item.split('=')[0], val = item.split('=')[1];
      res[key] = decodeURIComponent(val);
    })
  }
  return res
}
/* Test Code */
const user = getUrlParams("http://www.ok.com?user='admin'&id=12345");
console.log(user);

Image Lazy Loading

Element.getBoundingClientRect() 方法返回一个 DOMRect 对象,其提供了元素的大小及其相对于视口的位置。

The dataset read-only property of the HTMLElement interface provides read/write access to custom data attributes (data-*) on elements. It exposes a map of strings (DOMStringMap) with an entry for each data-* attribute.

<img src="imgs/default.png" data-src="https://.../real-image.png" />
function isVisible(el){
  const position = el.getBoundingClientRect();
  const windowHeight = document.documentElement.clientHeight; // clientHeight 为元素内部的高度 => 在根元素或怪异模式下的元素上使用 clientHeight 时返回视口高度
  const topVisible = position.top > 0 && position.top < windowHeight;
  const bottomVisible = position.top < windowHeight && position.bottom > 0;
  return topVisible || bottomVisible;
}
function imageLazyLoad(){
  const images = document.querySelectorAll("img");
  for(let img of images){
    const realSrc = img.dataset.src;
    if(!realSrc) continue
    if(isVisible(img)){
      img.src = realSrc;
      img.dataset.src = '';
    }
  }
}
/* Test Code */
window.addEventListener("load", imageLazyLoad);
window.addEventListener("scroll", imageLazyLoad);

结束

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!