JS_手写实现
不是所有的努力都值得表扬,除非它能带来成效
大家好,我是柒八九。
今天,我们继续前端面试的知识点。我们来谈谈关于JS手写的相关知识点和具体的算法。
该系列的文章,大部分都是前面文章的知识点汇总,如果想具体了解相关内容,请移步相关系列,进行探讨。
如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。
文章list
好了,天不早了,干点正事哇。
你能所学到的知识点
apply
&bind
&call
new
- ES5、ES6继承
instanceof
debounce
&throttle
reduce
compose
- 合并对象
- 函数柯里化
- 深复制对象
Object.create
- 函数缓存
- 数组去重
- 手写
Promise
Generator
实现Async/Await
- 观察者模式 (
Proxy
)- 发布订阅
Array.prototype.flat()
- 判断元素在可视区域
apply & bind & call (ABC)
apply & bind & call 原理介绍
apply
、bind
和call
是挂在 Function
对象上的三个方法,调用这三个方法的必须是一个函数
func.apply(thisArg, [param1,param2,...])
func.bind(thisArg, param1, param2, ...)
func.call(thisArg, param1, param2, ...)
func
是要调用的函数thisArg
一般为this
所指向的对象
共同点
- 都可以改变函数
func
的this
指向
不同点
call
VSapply
- 传参的写法不同
apply
的第 2 个参数为数组
bind
VS (call
和apply
)bind
虽然改变了func
的this
指向,但不是马上执行call
、apply
是在改变了函数的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])
}
}
总结
new
使用 new
调用类的构造函数会执行如下操作(三步)
- 在内存中创建一个新对象
- 这个新对象内部的
[[Prototype]]
指针被赋值为构造函数的prototype
属性 context = Object.create(constructor.prototype);
- 这个新对象内部的
- 构造函数内部的
this
被赋值为这个新对象(即 this 指向新对象)- 执行构造函数内部的代码(给新对象添加属性)
result = constructor.apply(context, params);
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象
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继承
通过
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()
方法将原型式继承的概念规范化
这个方法接收两个参数
- 作为新对象原型的对象,在只有一个参数时,
Object.create()
与object()
方法效果相同 - 给新对象定义额外属性的对象(可选)
寄生式继承 (过渡方式)
对原型式继承的二次封装,在二次封装中对继承的对象进行拓展。
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){
immediate == true
- 执行函数
fn.apply(this,args);
- 修改开关
immediate = false
- 执行函数
- 清除在阈值时间内触发的定时器
clearTimeout(timerId);
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
采用原理:连续操作时,上次设置的setTimeout
被clear
掉
- 返回一个函数(构成闭包)
return function(...args){
- 每次调用
debounceTail
函数时,用clearTimeout()
清除之前的定时器timerId
。clearTimeout(timeoutId);
- 使用
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
闭包版本
- 在当前函数作用域定义了一个空对象,用于缓存运行结果
- 运用柯里化返回一个函数,返回的函数由于闭包特性,可以访问到
cache
- 然后判断输入参数是不是在
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);
}
});
数组去重
- 双循环去重
indexOf
方法- 相邻元素去重
- 利用对象属性去重
set
与解构赋值去重Array.from
与set
去重
双循环去重
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
的调用流程:
Promise
的构造方法接收一个executor()
,在new Promise()
时就立刻执行这个executor
回调executor()
内部的异步任务被放入宏/微任务队列,等待执行then()
被执行,收集成功/失败回调,放入成功/失败队列executor()
的异步任务被执行,触发resolve/reject
,从成功/失败队列中取出回调依次执行
其实熟悉设计模式,很容易就能意识到这是个观察者模式,这种
- 收集依赖
- 触发通知
- 取出依赖执行
的方式,被广泛运用于观察者模式的实现,在 Promise
里,执行顺序是
then
收集依赖- 异步触发
resolve
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
(生成器)的封装,是一个语法糖。
*/yield
和async/await
看起来其实已经很相似了,它们都提供了暂停执行的功能,但二者又有三点不同:
async/await
自带执行器,不需要手动调用next()
就能自动执行下一步async
函数返回值是Promise
对象,而Generator
返回的是生成器对象await
能够返回Promise
的resolve/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)
// 定义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 ="前端柒八九"
// 触发了
// 前端柒八九
发布订阅
发布订阅核心点
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 发布订阅模式
- 从表面上看:
- 观察者模式里,只有两个角色 —— 观察者 + 被观察者
- 而发布订阅模式里,却不仅仅只有发布者和订阅者两个角色,还有一个经常被我们忽略的 —— {经纪人|Broker}
- 往更深层次讲:
- 观察者和被观察者,是松耦合的关系
- 发布者和订阅者,则完全不存在耦合
- 从使用层面上讲:
- 观察者模式,多用于单个应用内部
- 发布订阅模式,则更多的是一种{跨应用的模式|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;
}
判断元素在可视区域
offsetXX
、scrollXX
、clientXX
getBoundingClientRect
Intersection Observer
offsetXX + scrollXX + clientXX
offset家族
offsetHeight 和 offsetWidth
scroll家族
scrollHeight和scrollWidth (只读)
获取元素整个内容的高度和宽度 (包含看不见的) ,如果有滚动条(滚动条会占用部分宽高),不计算滚动条的宽高
Element.scrollHeight
这个只读属性是一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容
scrollTop和scrollLeft(可修改)
获取元素垂直和水平滚动条滚动的距离(被卷去的头部和左侧)
client家族
clientHeight和clientWidth(不包含滚动条)
clientHeight
:元素的可见高度- 包括元素的内容区和内边距的高度
- 即
clientHeight = content + padding
clientWidth
:元素的可见宽度- 包括元素的内容区和内边距的宽度
- 即
clientWidth = content + padding
clientLeft和ClientTop
边框宽度和边框的高度
判断滚动条是否滚动到底
-
垂直滚动条
scrollHeight -scrollTop = clientHeight
-
水平滚动
scrollWidth -scrollLeft = clientWidth
总结
判断一个元素是否在可视窗口内
0 <= el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
1
- 目标元素的
offsetTop
- document元素的
scrollTop
- 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
属性
当页面发生滚动的时候,top
与 left
属性值都会随之改变.
如果一个元素在视窗之内的话,那么它一定满足下面四个条件:
- top 大于等于 0
- left 大于等于 0
- bottom 小于等于视窗高度
- 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
会好很多
使用步骤主要分为两步:
- 创建观察者
- 传入被观察者
创建观察者
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();
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。
转载自:https://juejin.cn/post/7156484807873003534