likes
comments
collection
share

JS_手写实现

作者站长头像
站长
· 阅读数 14

不是所有的努力都值得表扬,除非它能带来成效

大家好,我是柒八九

今天,我们继续前端面试的知识点。我们来谈谈关于JS手写的相关知识点和具体的算法。

该系列的文章,大部分都是前面文章的知识点汇总,如果想具体了解相关内容,请移步相关系列,进行探讨。

如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。

文章list

  1. CSS重点概念精讲
  2. JS_基础知识点精讲
  3. 网络通信_知识点精讲

好了,天不早了,干点正事哇。 JS_手写实现

你能所学到的知识点

  1. apply & bind & call
  2. new
  3. ES5、ES6继承
  4. instanceof
  5. debounce & throttle
  6. reduce
  7. compose
  8. 合并对象
  9. 函数柯里化
  10. 深复制对象
  11. Object.create
  12. 函数缓存
  13. 数组去重
  14. 手写Promise
  15. Generator 实现
  16. Async/Await
  17. 观察者模式 (Proxy)
  18. 发布订阅
  19. Array.prototype.flat()
  20. 判断元素在可视区域

apply & bind & call (ABC)

apply & bind & call 原理介绍

applybindcall 是挂在 Function 对象上的三个方法,调用这三个方法的必须是一个函数

func.apply(thisArg, [param1,param2,...])

func.bind(thisArg, param1, param2, ...)

func.call(thisArg, param1, param2, ...)

  • func 是要调用的函数
  • thisArg 一般为 this 所指向的对象

共同点

  • 都可以改变函数 functhis 指向

不同点

  • call VS apply
    • 传参的写法不同
    • apply 的第 2 个参数为数组
  • bind VS (callapply
    • bind 虽然改变了 functhis 指向,但不是马上执行
    • callapply 是在改变了函数的 this 指向之后立马执行

apply 和 call 的实现

  • Function.prototype上定义指定方法
  • 函数中的this就是被调用函数本身

apply

Function.prototype.myApply = function (context, args) {
  context = context || window;
  context.fn = this;
  let result = context.fn(...args);
  delete context.fn
  return result;
}

call

Function.prototype.myCall = function (context, ...args) {
  context = context || window;
  context.fn = this;
  let result = context.fn(...args);
  delete context.fn
  return result;
}

bind 的实现

Function.prototype.myBind = function (context,...arg1) {
    let that = this
    return function(...arg2){
      return that.apply(context, [...arg1, ...arg2])
    }
}

总结

JS_手写实现


new

使用 new 调用类的构造函数会执行如下操作(三步)

  1. 在内存中创建一个新对象
    • 这个新对象内部的[[Prototype]]指针被赋值为构造函数的 prototype 属性
    • context = Object.create(constructor.prototype);
  2. 构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)
    • 执行构造函数内部的代码(给新对象添加属性)
    • result = constructor.apply(context, params);
  3. 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
    • return (typeof result === 'object' && result != null) ? result : context;
 function _new(
     /* 构造函数 */ constructor, 
     /* 构造函数参数 */ ...params
     ) {
    // 创建一个空对象,继承构造函数的 prototype 属性
    let context = Object.create(constructor.prototype);
    // 执行构造函数
    let result = constructor.apply(context, params);
    // 如果返回结果是对象,就直接返回,否则返回 context 对象
    return (typeof result === 'object' && result != null) 
          ? result 
          : context;
}

ES5、ES6继承

JS_手写实现 通过Object.create 来划分不同的继承方式,最后的寄生式组合继承方式是通过组合继承改造之后的最优继承方式,而 extends 的语法糖和寄生组合继承的方式基本类似

ES5 继承 (6类)

原型继承

将父类实例赋值给子类的原型对象(prototype)

SubClass.prototype = new SuperClass()

缺点

  • 父类中引用类型的属性,被子类实例公用
  • 创建父类的时候,无法向父类传递参数

构造函数继承(借助 call)

创建即继承
function SubClass(params){
  SuperClass.call(this,params)
}

缺点

  • 不能继承原型属性或者方法

组合继承

原型继承 + 构造函数继承

function SubClass(name){
    // 构造函数式继承父类name属性
   SuperClass.call(this,name)
}
// 原型链继承 子类原型继承父类实例
SubClass.prototype = new SuperClass();
// 修正因为重写子类原型导致子类的constructor属性被修改
SubClass.prototype.constructor = subClass;

缺点

  • 父类构造函数被调用两次

原型式继承

对原型链继承的封装,过渡对象相对于原型继承的子类

function inheritObject(o){
   //声明一个过渡函数对象
   function F(){}
   //过渡函数的原型继承父对象
   F.prototype = o;
   // 返回一个实例,该实例的原型继承了父对象
   return new F();
}

缺点

  • 父类中引用类型的属性,被子类实例公用

ECMAScript 5 通过增加 Object.create()方法将原型式继承的概念规范化

这个方法接收两个参数

  1. 作为新对象原型的对象,在只有一个参数时,Object.create()object()方法效果相同
  2. 给新对象定义额外属性的对象(可选)

寄生式继承 (过渡方式)

原型式继承的二次封装,在二次封装中对继承的对象进行拓展。

function createObject(obj){
   //通过原型式继承创建新对象
   var o =Object.create(obj);
   //拓展新对象
   o.name = `北宸南蓁`;
   //返回拓展后的对象
   return o;
}

缺点

  • 父类中引用类型的属性,被子类实例公用

寄生组合式继承

function inheritPrototype(subClass, superClass) {
  //复制一份父类的原型副本并赋值给子类原型
  subClass.prototype  =Object.create(superClass.prototype);
  // 修正因为重写子类原型导致子类的constructor属性被修改
  subClass.prototype.constructor = subClass;
}

function subClass(){
  superClass.call(this)
}

ES6 的 extends 关键字 也采用这种方式


ES6继承

原理:ES6类 + 寄生式组合继承

es5和es6的继承有什么区别

ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(superClass.call(this)).

ES6的继承机制完全不同,实质上是先创建父类的实例对象this(所以必须先调用父类的super()方法),然后再用子类的构造函数修改this

ES5的继承时通过原型或构造函数机制来实现。

ES6通过class关键字定义类,里面有构造方法,类之间通过extends关键字实现继承。子类必须在constructor方法中调用super方法,否则新建实例报错。因为子类没有自己的this对象,而是继承了父类的this对象,然后对其进行加工。如果不调用super方法,子类得不到this对象。


instanceof

function _instance_of(L, R) { 
    let RP = R.prototype; // 取右表达式的 prototype 值
    L = L.__proto__; // 取左表达式的__proto__值
    while (true) {
        if (L === null) return false;
        if (L === RP) return true;
        L = L.__proto__ 
    }
}

测试案例

let a = []
let b = {}

console.log(_instance_of(a, Array)) // true
console.log(_instance_of(b, Object)) // true

debounce & throttle

  • 连续操作:两个操作之间的时间间隔小于设定的阀值,这样子的一连串操作视为连续操作。
  • debounce(防抖):一个连续操作中的处理,只触发一次,从而实现防抖动。
  • throttle(节流):一个连续操作中的处理,按照阀值时间间隔进行触发,从而实现节流。

debounceStart

核心点

  • immediate用于控制是否第一次触发 (默认为true
  • 返回子函数(构成闭包) return function(...args){
    1. immediate == true
      1. 执行函数 fn.apply(this,args);
      2. 修改开关 immediate = false
    2. 清除在阈值时间内触发的定时器
      • clearTimeout(timerId);
    3. A触发后,在A + 阈值时间(ms)后,为了 B触发能启动,需要将immediate重置
      • timerId = setTimeout(()=>{ immediate = true },ms)
const debounceStart = (fn,ms =0) => {
    let immediate = true;
    let timerId = null;

    return function(...args){
        if(immediate) {
            fn.apply(this,args);
            immediate = false
        }

        clearTimeout(timerId);

        timerId = setTimeout(()=>{ 
              immediate = true 
            }
        ,ms)
    }
}

debounceTail

采用原理:连续操作时,上次设置的setTimeoutclear

  1. 返回一个函数(构成闭包)
    • return function(...args){
  2. 每次调用debounceTail函数时,用clearTimeout()清除之前的定时器timerId
    • clearTimeout(timeoutId);
  3. 使用setTimeout()创建一个新的定时器,将函数的调用延迟到 ms毫秒后。
    • timerId = setTimeout(() => fn.apply(this, args), ms);
    • 使用Function.prototype.apply()this上下文应用于fn并提供必要的args
    • 省略第二个参数(ms),将超时设置为默认的0ms。
const debounceTail = (fn, ms = 0) => {
  let timerId;
  return function(...args) {
    clearTimeout(timeoutId);
    timerId = setTimeout(() => 
          fn.apply(this, args)
          ,ms);
  };
};

throttle

简单版本 ==> 代码和debounceTail很像。

在存在timerId时候,不是clear而是直接返回return

function throttle(fn, ms = 0){
  let timerId;
  return function(...args){
      if(timerId) return;
      timerId = setTimeout(()=>{
          fn.apply(this, args); 
          timerId = null;
      }, ms)
  }
}

reduce

常用的函数签名

reduce(
  (previousValue, currentValue, currentIndex, array) => { /* ... */ },
  initialValue
  )

reduce() 方法对数组中的每个元素按序执行一个由你提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值。

一个 reducer 函数,包含四个参数:

  • previousValue:上一次调用 reducer 时的返回值。
    • 在第一次调用时,若指定了初始值 initialValue,其值则为 initialValue
    • 否则为数组索引为 0 的元素 array[0]
  • currentValue:数组中正在处理的元素。
    • 在第一次调用时,若指定了初始值 initialValue,其值则为数组索引为 0 的元素 array[0]
    • 否则为 array[1]
  • currentIndex:数组中正在处理的元素的索引。
    • 若指定了初始值 initialValue,则起始索引号为 0,
    • 否则从索引 1 起始。
  • array:用于遍历的数组。
Array.prototype.myReduce = function(reducer, initialValue) {
    const hasInitial = initialValue !== 'undefined';
    let ret = hasInitial ? initialValue : this[0];
    for (let i = hasInitial ? 0 : 1; i < this.length; i++) {
        ret = reducer(ret, this[i], i, this);
    }
    return ret;
}
将二维数组转化为一维数组
let flattened = [[0, 1], [2, 3], [4, 5]].myReduce(
  ( previousValue, currentValue ) => previousValue.concat(currentValue),
  []
)

数组去重
let myArray = ['a', 'b', 'a', 'b', 'c', 'e', 'e', 'c', 'd', 'd', 'd', 'd']
let myArrayWithNoDuplicates = myArray.myReduce(
  function (previousValue, currentValue) {
    if (previousValue.indexOf(currentValue) === -1) {
      previousValue.push(currentValue)
    }
    return previousValue
  }, 
[])


compose

compose 可以把类似于f(g(h(x)))这种写法简化成compose(f, g, h)(x)

const compose = (...fns) =>
  fns.reduce((f, g) => (...args) => f(g(...args)));

调用

const add1 = (x) => x + 1;
const mul3 = (x) => x * 3;
const div2 = (x) => x / 2;
div2(mul3(add1(5))); //=> 9

let result = compose(div2, mul3, add1)(5);
console.log(result); // =>9

合并对象

从两个或多个对象的组合中创建一个新对象。

  • 针对数组对象和数组的数据进行合并处理
  • 使用Array.prototype.reduce()结合Object.keys()来遍历所有对象和键。
  • 使用Object.prototype.hasOwnProperty()Array.prototype.concat()来追加多个对象中存在的键的值。
const merge = (...objs) =>
  [...objs].reduce(
    (acc, obj) =>
      Object.keys(obj).reduce((a, k) => {
        acc[k] = acc.hasOwnProperty(k)
          ? [].concat(acc[k]).concat(obj[k])
          : obj[k];
        return acc;
      }, {}),
    {}
  );

示例:

const object = {
  a: [{ x: 2 }, { y: 4 }],
  b: 1
};
const other = {
  a: { z: 3 },
  b: [2, 3],
  c: 'foo'
};
merge(object, other);
// { a: [ { x: 2 }, { y: 4 }, { z: 3 } ], b: [ 1, 2, 3 ], c: 'foo' }

函数柯里化

  • 使用递归。
  • 如果提供的参数(args)的数量足够多,就调用被处理的函数fn
  • 否则,使用Function.prototype.bind()来返回一个接收其余参数的curry函数fn
  • 如果你想柯里化一个接受可变参数数的函数(一个变量函数,例如Math.min()),你可以选择将参数数传递给第二个参数arity
const curry = (fn, arity = fn.length, ...args) =>
  arity <= args.length ? fn(...args) : curry.bind(null, fn, arity, ...args);

示例

curry(Math.pow)(2)(10); // 1024
curry(Math.min, 3)(10)(50)(2); // 2

深复制对象

创建一个对象的深度克隆。克隆基本数据类型、数组和对象,不包括类实例。

  • 使用递归。
  • 检查传递的对象是否为空,如果是,则返回空。
  • 使用Object.assign()和一个空对象({})来创建一个原始对象的浅层克隆。
  • 使用Object.keys()Array.prototype.forEach()来确定哪些键值对需要被深度克隆。
  • 如果对象是一个数组,将克隆的长度设置为原始对象的长度,并使用Array.from()来创建一个克隆。
const deepClone = obj => {
  if (obj === null) return null;
  let clone = Object.assign({}, obj);
  Object.keys(clone).forEach(
    key =>
      (clone[key] =
        typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
  );
  if (Array.isArray(obj)) {
    clone.length = obj.length;
    return Array.from(clone);
  }
  return clone;
};

示例:

const a = { foo: 'bar', obj: { a: 1, b: 2 } };
const b = deepClone(a); // a !== b, a.obj !== b.obj

Object.create

函数签名

Object.create(proto)
Object.create(proto, propertiesObject)

const create = function (proto) {
    if (typeof proto !== "object" && typeof proto !== "function") {
        // 类型校验
        throw new TypeError("proto必须为对象或者函数");
    } else if (proto === null) {
        // null 特殊处理
        throw new Error("在浏览器中暂不支持传递null");
    }

    // 创建一个构造函数
    function F() {}
    // 更改其 prototype
    F.prototype = proto;

    // 返回构造的实例, 这个时候返回的实例和传入的 proto中间多了一层 F
    return new F();
};

函数缓存

函数缓存,就是将函数运算过的结果进行缓存

本质上就是用空间(缓存存储)换时间(计算过程)

常用于缓存数据计算结果和缓存对象。

Memoization 是一种常用的技术,可以帮助大大加快你的代码速度。这种技术依靠一个缓存来存储以前完成的工作单元的结果。

缓存的目的是为了避免多次执行相同的工作,从而加快耗时函数的后续调用。

基于这个定义,我们可以很容易地提取一些标准,帮助我们决定何时在代码中使用记忆化。

  • Memoization 主要在加快性能缓慢、成本高或耗时的函数调用方面很有用
  • 记忆化可以加速后续的调用,所以当你预计在相同情况下多次调用同一个函数时,最好使用记忆化。
  • Memoization将结果存储在内存中,所以当同一函数在不同的情况下被多次调用时,应该避免使用它。
const add = (a,b) => a+b;
const calc = memoize(add); // 函数缓存
calc(10,20);// 30
calc(10,20);// 30 缓存

// ====
const fibonacci = n => (n <= 2 ? 1 : fibonacci(n - 1) + fibonacci(n - 2));
const memoizedFibonacci = memoize(fibonacci);

for (let i = 0; i < 100; i ++)
  fibonacci(30);                      // ~5000ms
for (let i = 0; i < 100; i ++)
  memoizedFibonacci(30);              // ~50ms

闭包版本

  1. 在当前函数作用域定义了一个空对象,用于缓存运行结果
  2. 运用柯里化返回一个函数,返回的函数由于闭包特性,可以访问到cache
  3. 然后判断输入参数是不是在cache的中。
    • 如果已经存在,直接返回cache的内容,
    • 如果没有存在,使用函数func对输入参数求值,然后把结果存储在cache =
let memoize = function (func, context) {
  let cache = Object.create(null)
  context = context || this
  return (...key) => {
    if (!cache[key]) {
      cache[key] = func.apply(context, key)
    }
    return cache[key]
  }
}

Proxy 版本

const memoize = fn => new Proxy(fn, {
  cache: new Map(),
  apply (target, thisArg, argsList) {
    let cacheKey = argsList.toString();
    if(!this.cache.has(cacheKey))
      this.cache.set(cacheKey, target.apply(thisArg, argsList));
    return this.cache.get(cacheKey);
  }
});



数组去重

  1. 双循环去重
  2. indexOf方法
  3. 相邻元素去重
  4. 利用对象属性去重
  5. set与解构赋值去重
  6. Array.fromset去重

双循环去重

function unique(arr) {
    let res = [arr[0]]
    for (let i = 1; i < arr.length; i++) {
        let flag = true
        for (let j = 0; j < res.length; j++) {
            if (arr[i] === res[j]) {
                flag = false;
                break
            }
        }
        if (flag) {
            res.push(arr[i])
        }
    }
    return res
}

indexOf方法

 function unique(arr) {
    let res = []
    for (let i = 0; i < arr.length; i++) {
        if (res.indexOf(arr[i]) === -1) {
            res.push(arr[i])
        }
    }
    return res
}

相邻元素去重

function unique(arr) {
    arr = arr.sort((a,b)=>a-b)
    let res = []
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] !== arr[i-1]) {
            res.push(arr[i])
        }
    }
    return res
}

利用对象属性去重

function unique(arr) {
    let res = [],
        obj = {};
    for (let i = 0; i < arr.length; i++) {
        if (!obj[arr[i]]) {
            res.push(arr[i])
            obj[arr[i]] = 1
        } else {
            obj[arr[i]]++
        }
    }
    return res
}

set与解构赋值去重

数组去重

 function unique(arr) {
    return [...new Set(arr)]
}

字符串去重

// 字符串
let str = "352255";
let str1 = "abbcc"
let unique = [...new Set(str)].join(""); 
// "352"
// "abc"

Array.from与set去重

function unique(arr) {
    return Array.from(new Set(arr))
}


手写Promise

Promise 对象就是为了解决回调地狱而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用

分析 Promise 的调用流程:

  1. Promise 的构造方法接收一个executor(),在new Promise()时就立刻执行这个 executor 回调
  2. executor()内部的异步任务被放入宏/微任务队列,等待执行
  3. then()被执行,收集成功/失败回调,放入成功/失败队列
  4. executor()异步任务被执行,触发resolve/reject,从成功/失败队列中取出回调依次执行

其实熟悉设计模式,很容易就能意识到这是个观察者模式,这种

  • 收集依赖
  • 触发通知
  • 取出依赖执行

的方式,被广泛运用于观察者模式的实现,在 Promise 里,执行顺序是

  1. then收集依赖
  2. 异步触发resolve
  3. resolve执行依赖。

依此,我们可以勾勒出 Promise 的大致形状:

//Promise/A+规范的三种状态
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class MyPromise {
  // 构造方法接收一个回调
  constructor(executor) {
    this._status = PENDING     // Promise状态
    this._resolveQueue = []    // 成功队列, resolve时触发
    this._rejectQueue = []     // 失败队列, reject时触发

    // 由于resolve/reject是在executor内部被调用, 因此需要使用箭头函数固定this指向, 否则找不到this._resolveQueue
    let _resolve = (val) => {
      if(this._status !== PENDING) return// 对应规范中的"状态只能由pending到fulfilled或rejected"
      this._status = FULFILLED              // 变更状态

      // 这里之所以使用一个队列来储存回调,是为了实现规范要求的 "then 方法可以被同一个 promise 调用多次"
      // 如果使用一个变量而非队列来储存回调,那么即使多次p1.then()也只会执行一次回调
      while(this._resolveQueue.length) {
        const callback = this._resolveQueue.shift()
        callback(val)
      }
    }
    // 实现同resolve
    let _reject = (val) => {
      if(this._status !== PENDING) return// 对应规范中的"状态只能由pending到fulfilled或rejected"
      this._status = REJECTED               // 变更状态
      while(this._rejectQueue.length) {
        const callback = this._rejectQueue.shift()
        callback(val)
      }
    }
    // new Promise()时立即执行executor,并传入resolve和reject
    executor(_resolve, _reject)
  }

  // then方法,接收一个成功的回调和一个失败的回调
  then(resolveFn, rejectFn) {
    this._resolveQueue.push(resolveFn)
    this._rejectQueue.push(rejectFn)
  }
}

代码测试

const p1 = new MyPromise((resolve, reject) => {
  setTimeout(() => {
    resolve('result')
  }, 1000);
})
p1.then(res =>console.log(res))
//一秒后输出result

Generator 实现

function* foo() {
  yield'result1'
  yield'result2'
  yield'result3'
}

const gen = foo()
console.log(gen.next().value)
console.log(gen.next().value)
console.log(gen.next().value)

我们可以在 babel 官网上在线转化这段代码,看看 ES5 环境下是如何实现 Generator 的:

"use strict";

var _marked =
/*#__PURE__*/
regeneratorRuntime.mark(foo);

function foo() {
  return regeneratorRuntime.wrap(function foo$(_context) {
    while (1) {
      switch (_context.prev = _context.next) {
        case0:
          _context.next = 2;
          return'result1';

        case2:
          _context.next = 4;
          return'result2';

        case4:
          _context.next = 6;
          return'result3';

        case6:
        case"end":
          return _context.stop();
      }
    }
  }, _marked);
}

var gen = foo();
console.log(gen.next().value);
console.log(gen.next().value);
console.log(gen.next().value);
  • mark()方法为生成器函数(foo)绑定了一系列原型
  • wrap()相当于是给 generator 增加了一个_invoke 方法

Generator 实现的核心在于上下文的保存,函数并没有真的被挂起,每一次 yield,其实都执行了一遍传入的生成器函数,只是在这个过程中间用了一个 context 对象储存上下文,使得每次执行生成器函数的时候,都可以从上一个执行结果开始执行,看起来就像函数被挂起了一样


迭代原生对象

function* objectEntries(obj){
  let propKeys = Reflect.ownKeys(obj);
  for(let propKey  of propKeys){
      yield [propKey,obj[propKey]]
  }
}

利用for-of迭代对象

let obj = { first: 'wl', age: 789 };

for (let [key, value] of objectEntries(obj)) {
  console.log(`${key}: ${value}`);
}

Async/Await

async/await 实际上是对 Generator(生成器)的封装,是一个语法糖

*/yieldasync/await看起来其实已经很相似了,它们都提供了暂停执行的功能,但二者又有三点不同:

  1. async/await自带执行器,不需要手动调用 next()就能自动执行下一步
  2. async 函数返回值是 Promise 对象,而 Generator 返回的是生成器对象
  3. await 能够返回 Promiseresolve/reject 的值

不管await后面跟着的是什么,await都会阻塞后面的代码

async function fn1 (){
    console.log(1)
    await fn2()
    console.log(2) // {A}
}

async function fn2 (){
    console.log('fn2')
}

fn1()
console.log(3)

await 会阻塞{A}及其以后的代码(即加入微任务队列),先执行 async 外面的同步代码,同步代码执行完,再回到 async 函数中,再执行之前阻塞的代码

上述输出结果为:1,fn2,3,2


观察者模式 (Proxy)

JS_手写实现

// 定义observe
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);

// 定义Subject
function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  // notify
  queuedObservers.forEach(observer => observer());
  return result;
}
const observable = obj => new Proxy(obj, {set});


observe(function test(){
  console.log('触发了')
})

obj = observable({
  name:'789'
})

obj.name ="前端柒八九"
// 触发了
// 前端柒八九

发布订阅

JS_手写实现

发布订阅核心点

  • on:订阅
    • this.caches[eventName].push(fn);
  • emit:发布
    • this.caches[eventName].forEach(fn => fn(data));
  • off:取消订阅
    • this.caches[eventName]filter出与fn不同的函数
    • const newCaches = fn ? this.caches[eventName].filter(e => e !== fn) : [];
    • this.caches[eventName] = newCaches;
  • 内部需要一个单独事件中心caches进行存储
class Observer {
  caches = {}; // 事件中心
  
  // eventName事件名-独一无二, fn订阅后执行的自定义行为
  on (eventName, fn){ 
    this.caches[eventName] = this.caches[eventName] || [];
    this.caches[eventName].push(fn);
  }
  
  // 发布 => 将订阅的事件进行统一执行
  emit (eventName, data) { 
    if (this.caches[eventName]) {
      this.caches[eventName]
      .forEach(fn => fn(data));
    }
  }
  // 取消订阅 => 若fn不传, 直接取消该事件所有订阅信息
  off (eventName, fn) { 
    if (this.caches[eventName]) {
      const newCaches = fn 
        ? this.caches[eventName].filter(e => e !== fn) 
        : [];
      this.caches[eventName] = newCaches;
    }
  }

}

测试用例

ob = new Observer();

l1 = (data) => console.log(`l1_${data}`)
l2 = (data) => console.log(`l2_${data}`)

ob.on('event1',l1)
ob.on('event1',l2)

//发布订阅
ob.emit('event1',789) 
// l1_789
// l2_789

// 取消,订阅l1
ob.off('event1',l1)

ob.emit('event1',567)
//l2_567

观察者模式 VS 发布订阅模式

JS_手写实现

  1. 从表面上看:
    • 观察者模式里,只有两个角色 —— 观察者 + 被观察者
    • 而发布订阅模式里,却不仅仅只有发布者和订阅者两个角色,还有一个经常被我们忽略的 —— {经纪人|Broker}
  2. 往更深层次讲:
    • 观察者和被观察者,是松耦合的关系
    • 发布者和订阅者,则完全不存在耦合
  3. 从使用层面上讲:
    • 观察者模式,多用于单个应用内部
    • 发布订阅模式,则更多的是一种{跨应用的模式|cross-application pattern} ,比如我们常用的消息中间件

Array.prototype.flat()

flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

var newArray = arr.flat([depth])

展开一层数组

arr = [1, 2, [3, 4]];
arr.flat();

reduce + concat

arr.reduce((acc, val) => acc.concat(val), []);
// [1, 2, 3, 4]

扩展运算符 ...

const flattened = arr => [].concat(...arr);

展开N层数组

function eachFalt(arr,depth = 1){
  const result = [];
  (function flat(arr,depth){
    arr.forEach(item => {
      if(Array.isArray(item) && depth >0){
        flat(item,depth -1)
      }else{
        result.push(item)
      }
    })
  })(arr,depth);
  return result;
}

迭代

function flatten(arr){
  const stack = [...arr];
  let result = [];
  while(stack.length){
    // 使用 pop 从 stack 中取出并移除值
    const next = stack.pop();
    if(Array.isArray(next)){
      // 使用 push 送回内层数组中的元素,不会改动原始输入
      stack.push(...next)
    }else{
      result.push(next)
    }
  }
  // 反转恢复原数组的顺序
  return result.reverse();
}

计算数组的深度(递归版本)

const eachDepth = (arr) => {
  let count = 1;
  (function depth(arr){
    arr.forEach(item=>{
      if(Array.isArray(item)){
        count++;
        depth(item)
      }
    })
  })(arr)
  return count;
}

判断元素在可视区域

  1. offsetXXscrollXXclientXX
  2. getBoundingClientRect
  3. Intersection Observer

offsetXX + scrollXX + clientXX

JS_手写实现

offset家族

JS_手写实现

offsetHeight 和 offsetWidth

JS_手写实现

scroll家族

scrollHeight和scrollWidth (只读)

获取元素整个内容的高度和宽度 (包含看不见的) ,如果有滚动条(滚动条会占用部分宽高),不计算滚动条的宽高

JS_手写实现 Element.scrollHeight 这个只读属性是一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容

scrollTop和scrollLeft(可修改)

获取元素垂直和水平滚动条滚动的距离(被卷去的头部和左侧)

client家族

clientHeight和clientWidth(不包含滚动条)

JS_手写实现

  • clientHeight:元素的可见高度
    • 包括元素的内容区和内边距的高度
    • clientHeight = content + padding
  • clientWidth:元素的可见宽度
    • 包括元素的内容区和内边距的宽度
    • clientWidth = content + padding

clientLeft和ClientTop

边框宽度和边框的高度

JS_手写实现

判断滚动条是否滚动到底

  • 垂直滚动条 scrollHeight -scrollTop = clientHeight

  • 水平滚动 scrollWidth -scrollLeft = clientWidth


总结

JS_手写实现

判断一个元素是否在可视窗口内

0 <= el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
1

  1. 目标元素的offsetTop
  2. document元素的 scrollTop
  3. document或者body元素的 clientHeight
function isInViewPort (el) {
    // viewPortHeight 兼容所有浏览器写法
    const viewPortHeight = window.innerHeight 
                          || document.documentElement.clientHeight 
                          || document.body.clientHeight;
    const offsetTop = el.offsetTop
    const scrollTop = document.documentElement.scrollTop
    const top = offsetTop - scrollTop
    return top <= viewPortHeight && top >= 0
}

el.getBoundingClientRect()

返回值是一个 DOMRect 对象,拥有left, top, right, bottom, x, y, width, 和 height 属性

JS_手写实现

当页面发生滚动的时候,topleft 属性值都会随之改变.

如果一个元素在视窗之内的话,那么它一定满足下面四个条件:

  1. top 大于等于 0
  2. left 大于等于 0
  3. bottom 小于等于视窗高度
  4. right 小于等于视窗宽度
function isInViewPort(element) {
  const viewWidth = window.innerWidth 
                    || document.documentElement.clientWidth;
  const viewHeight = window.innerHeight 
                    || document.documentElement.clientHeight;
  const {
    top,
    right,
    bottom,
    left,
  } = element.getBoundingClientRect();

  return (
    top >= 0 &&
    left >= 0 &&
    right <= viewWidth &&
    bottom <= viewHeight
  );
}

Intersection Observer

Intersection Observer 即重叠观察者,因为不用进行事件的监听,性能方面相比getBoundingClientRect会好很多

使用步骤主要分为两步:

  1. 创建观察者
  2. 传入被观察者

创建观察者

const options = {
  // 表示重叠面积占被观察者的比例,从 0 - 1 取值,
  // 1 表示完全被包含
  threshold: 1.0, 
};

const callback = function(entries, observer) { 
    entries.forEach(entry => {
        // 进行逻辑处理
    });
};

const observer = new IntersectionObserver(callback, options);

entry 的各个属性

属性解释
entry.time触发的时间
entry.rootBounds根元素的位置矩形,这种情况下为视窗位置
entry.boundingClientRect被观察者的位置矩形
entry.intersectionRect重叠区域的位置矩形
entry.intersectionRatio重叠区域占被观察者面积的比例
entry.target被观察者

传入被观察者

const target = document.querySelector('.target');
observer.observe(target);

IntersectionObserver API 是异步的,不随着目标元素的滚动同步触发。

规格写明,IntersectionObserver 的实现,应该采用requestIdleCallback(),即只有线程空闲下来,才会执行观察器。这意味着,这个观察器的优先级非常低,只在其他任务执行完,浏览器有了空闲才会执行。


promiseQueue

class PromiseQueue {

  constructor(tasks=[],concurrentCount = 1){
    this.total = tasks.length;
    this.todo = tasks;
    this.running = [];
    this.complete = [];
    this.count = concurrentCount;
  }
  
  runNext(){
    return (
      (this.running.length < this.count) 
      && this.todo.length);
  }
  
  run() {
    while (this.runNext()) {
      const promise = this.todo.shift();
      promise.then(() => {
        this.complete.push(this.running.shift());
        this.run();
      });
      this.running.push(promise);
    }
  }
}
 

运行

// 接收一个promise数组,定义窗口大小为3
const taskQueue = new PromiseQueue(tasks, 3); 
taskQueue.run();

后记

分享是一种态度

全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。

JS_手写实现