GithubHelp home page GithubHelp logo

cxx-blog's Introduction

cxx-blog's People

Contributors

daytoywhy avatar

Stargazers

 avatar

Watchers

 avatar

cxx-blog's Issues

javascript实现继承的六种方式

方式一:通过原型链继承

这种方式是通过让子类的原型(prototype)是父类的实例(new Father),这样子类就可以使用父类所有的属性和方法

//父类型
function Father(name,age){
    this.name = name
    this.age = age
    this.nums = [1,2,3]
    this.setName = function () {}
}
Father.prototype.setAge = function () {}

//子类型
function Son(score) {
    this.score = score
    this.setScore = function () {}
}

//继承的方式
Son.prototype = new Father()
let s1 = new Son(100)
let s2 = new Son(60)
console.log(s1,s2)

通过让子类的原型指向父类的实例,这样子类的实例就可以通过__proto__访问到Son.prototype也就是父类的实例,这样就可以访问父类的方法和属性,然后再通过__proto__指向父类的prototype也就是父类的原型,这样就可以访问父类原型上的方法。这种方式让父类的公有、私有的属性和方法都变成了子类的公有属性和方法

这样就会产生一个问题,我们知道javascript的数据类型有基本类型和引用类型,操作基本数据类型的就是操作值本身,而操作引用数据类型实际上是操作引用的地址,通过原型链方式继承所有的子类实例都会共享父类的属性和方法,当子类实例操作父类引用数据时,就会影响其他子类实例

s1.nums.push(4)
console.log(s1.nums , s2.nums) //都是输出[1,2,3,4]
console.log(s1.__proto__ === s2.__proto__) // true
console.log(s1.__proto__.__proto__ === s2.__proto__.__proto__) // true

另外需要注意,在子类原型上想要新增属性或者方法或者想重写父类的属性和方法,一定要放在原型替换之后

Son.prototype.SetValue = function () {} //错误写法,原型替换父类实例会覆盖这一块内容
Son.prototype = new Father()
Son.prototype.SetValue = function () {} //正确写法

特点:

  • 父类新增原型方法/原型属性,子类都能访问到
  • 简单、易于实现

缺点:

  • 无法实现多继承
  • 父类的属性和方法被所有子类实例共享
  • 创建子类实例时,无法像父类传参数
  • 要想为子类新增属性和方法,必须要在Son.prototype = new Father() 之后执行,不能放到构造器中

方式二:通过构造函数实现继承

这种方式是在子类的构造函数中通过call()调用父类型的构造函数来实现

function Father(name,age) {
    this.age = age
    this.name = name
    this.setName = function () {}
}
Father.prototype.setAge = function () {}
function Son(name,age,socre){
    Father.call(name,age)  //这里相当于调用this.Father(name,age)
    /*this.name = name
       this.age = age*/
    this.score = score
}

let s1 = new Son('xiaoming' , '25' , '100')

这种方式只能实现部分继承,如果父类原型上还有方法和属性,子类实例是无法拿到的

console.log(s1.setAge())//Uncaught TypeError: s1.setAge is not a function

特点:

  • 创建子类实例时,可以向父类构造函数进行传参
  • 可以实现多继承,可以call()多个父类对象
  • 解决了原型链继承中多个子类实例共享父类引用属性的问题

缺点:

  • 只能继承父类的属性和方法,无法继承父类原型上的属性和方法
  • 实例并不是父类的实例,只是子类的实例
  • 无法实现函数复用,每个子类实例都有父类实例函数的副本,会影响性能

方式三:原型链+构造函的组合式继承

这种方式是结合方式一和方式二:通过调用父类函数,继承父类的属性和方法并保留可以向父类传参的优点,再让子类的原型指向父类的实例,实现函数的复用

//父类型
function Father(name,age){
    this.name = name
    this.age = age
    this.nums = [1,2,3]
    this.setName = function () {}
}
Father.prototype.setAge = function () {}

//子类型
function Son(name,age,score) {
    Father.call(this, name, age)  //利用方式二
    this.score = score
    this.setScore = function () {}
}

//继承的方式
Son.prototype = new Father() //利用方式一
Son.prototype.constructor = Son //组合式继承也需要修复构造函数指向问题
Son.prototype.setValue = function () {}
let s1 = new Son('xiaoming' , '25' , '100')
let s1 = new Son('xiaofang' , '24' , '150')
console.log(s1,s2)

特点:

  • 结合了方式一和方式二的特点,例如可向父类传参、不存在引用属性共享问题、可以继承父类实例的方法和属性,也可以继承其原型上的方法和属性
  • 函数可以复用

缺点:

  • 调用了两次父类的构造函数,生成了两份实例(一次是Son对象中调用Father.call(this),另一次是Son.prototype = new Father())

方式四:原型式继承

let Father = {
    a: 1
}

let Son = Object.create(Father)

console.log(Son)

特点:

  • 对父类进行浅克隆,继承父类共有和私有的属性和方法,简单易于实现

缺点:

  • 由于是浅克隆,也会造成操作父类引用数据对子类实例造成污染

方式五:寄生式继承

这种方式只是在原型继承的基础上做了增强

let Father = {
    name:'xiong'
}

function extand (0){ 
    const Son = Object.create(o)
    Son.prototype.setName = function() {}
    return Son
}

let s1 = extand(Father)
console.log(s1)

特点:

  • 通过克隆一个对象,并增强对象

缺点:

  • 无法实现函数的复用

方式六:寄生式组合继承

这种方式是通过借助原型可以基于已有的对象来创建对象,var B = Object.create(A)以A对象为原型,生成了B对象。B继承了A的所有属性和方法。

//父类型
function Father(name,age){
    this.name = name
    this.age = age
    this.nums = [1,2,3]
    this.setName = function () {}
}
Father.prototype.setAge = function () {}

//子类型
function Son(name,age,score) {
    Father.call(this, name, age) 
    this.score = score
    this.setScore = function () {}
}

//继承的方式
Son.prototype = Object.create(Father.prototype) // 关键代码
Son.prototype.constructor = Son //组合式继承也需要修复构造函数指向问题
Son.prototype.setValue = function () {}
let s1 = new Son('xiaoming' , '25' , '100')
let s1 = new Son('xiaofang' , '24' , '150')
console.log(s1,s2)

特点:

  • 不会调用两次父类的实例
  • 子类实例继承了父类所有原型的属性和方法,是目前最完美的继承方式

ES6 class继承

ES6中引入class关键字,并通过extends来实现继承

class Father {
    constructor(name,age){
        this.name = name
        this.age = age
    }
    setAge(){}
}

class Son extends Father{
    constructor(name,age,score){
        super(name,age) //这里要使用super调用父类的构造函数
        this.score = socre
    }
    setAge(){}
}

特点:

  • 语法简单,清晰明了

缺点:

  • 并不是所有浏览器都支持class关键字

Promise的原理及方法

前言

说到Promise,我们很容易联想到它可以解决回调地狱的问题,但它是通过什么方式来解决回调地狱问题的呢,请带着这样的疑惑,让我们一探究竟

Promise定义

在mdn中是这样定义Promise的:

一个 Promise 对象代表一个在这个 promise 被创建出来时不一定已知值的代理。它让你能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来。这样使得异步方法可以像同步方法那样返回值:异步方法并不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者。

它必定处于以下三种状态之一:

  • 待定(pending):初始状态,既没有被兑现,也没有被拒绝。
  • 已兑现(fulfilled):意味着操作成功完成。
  • 已拒绝(rejected):意味着操作失败。

Promise的链式调用

因为 Promise.prototype.then()Promise.prototype.catch()方法返回的都是Promise,所以它们可以被链式调用
我们可以用 Promise.prototype.then()Promise.prototype.catch()Promise.prototype.finally() 这些方法将进一步的操作与一个变为已敲定状态的 promise 关联起来。
例如在.then()方法中需要两个函数,一个是参数作为处理已兑现状态的回调函数,而第二个参数则作为处理已拒绝状态的回调函数,每个.then()方法返回的都是一个新的Promise对象,所以可以链式调用,如下

const myPromise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('foo');
  }, 300);
});

myPromise
  .then(handleResolvedA, handleRejectedA)
  .then(handleResolvedB, handleRejectedB)
  .then(handleResolvedC, handleRejectedC);

当 .then() 中缺少能够返回 promise 对象的函数时,链式调用就直接继续进行下一环操作。因此,链式调用可以在最后一个 .catch() 之前把所有的处理已拒绝状态的回调函数都省略掉。

过早地处理变为已拒绝状态的 promise 会对之后 promise 的链式调用造成影响。不过有时候我们因为需要马上处理一个错误也只能这样做。例如,外面必须抛出某种类型的错误以在链式调用中传递错误状态。另一方面,在没有迫切需要的情况下,可以在最后一个 .catch() 语句时再进行错误处理,这种做法更加简单。.catch() 其实只是没有给处理已兑现状态的回调函数预留参数位置的 .then() 而已。

myPromise
  .then(handleResolvedA)
  .then(handleResolvedB)
  .then(handleResolvedC)
  .catch(handleRejectedAny);

最后关于.finally()方法,一个Promise对象最终都会走进这个方法,工作中我发现有些实习同学很喜欢将原本放在成功回调里的一些方法放在.finnaly中去执行,这样是不对,无论Promise是成功还是失败的状态都会进入到.finally()方法中。一般在.finally()方法里我们可以执行像loading状态的取消这一类操作

手写Promise的实现

const PENDING = 'pending'
const FULFILED = 'fulfiled'
const REJECTED = 'rejected'

function myPromise(fn){
  var self = this
  this.state = PENDING
  this.value = null
  this.resolveCallback = []
  this.rejectCallback = []

  try {
    function resolve(value){
      if(value instanceof myPromise){
        return value.then(resolve,reject)
      }
      setTimeout(()=>{
        if(self.state === PENDING){
          self.state = FULFILED
          self.value = value
          self.resolveCallback.forEach(fn =>{
            fn(value)
          })
        }
      },0)
    }
  } catch (error) {
    console.log(error,'error');
  }
  function reject(value){
    setTimeout(()=>{
      if(self.state === PENDING){
        self.state = REJECTED
        self.value = value
        self.rejectCallback.forEach(fn => {
          fn(value)
        })
      }
    },0)
  }

  try {
    fn(resolve,reject)
  } catch (error) {
    reject(error)
  }
}

myPromise.prototype.then = function(onResolved,onRejected){
  onResolved = 
  typeof onResolved === 'function' 
  ? onResolved 
  : function(value){
    return value
  }
  onRejected = 
  typeof onRejected === 'function'
  ? onRejected
  : function(error){
    throw error
  } 
  if(this.state === PENDING){
    this.resolveCallback.push(onResolved)
    this.rejectCallback.push(onRejected)
  }
  if(this.state === FULFILED){
    onResolved(this.value)
  }
  if(this.state === REJECTED){
    onRejected(this.value)
  }
}

Promise.all()

Promise.all()方法接收一个数组类型的参数,数组里面一般是包含Promise的值,如果是非Promise的值,在执行过程中会忽略该参数,但会放在最终返回的数组上(Promise完成的话)

只有数组中所有Promise状态都是成功时,Promise.all()方法才会返回成功的状态,一旦数组中有一个失败的状态出现,则会立即返回失败的状态
以下是成功状态的返回

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).then((values) => {
  console.log(values); //  Array [3, 42, "foo"]
})

以下是失败状态的返回

const promise1 = Promise.resolve(3);
const promise2 = 42;
const promise3 = new Promise((resolve, reject) => {
  setTimeout(reject, 100, 'foo');
});

Promise.all([promise1, promise2, promise3]).catch((err) => {
  console.log(err); //  foo
})

手写Promise.all()方法

Promise.all = function(array){
    return new Promise((resolve,reject)=>{
    if(!array || array.length == 0){ resolve([]) }
    else{
    let count = 0,reuslt = [];
    for(let i = 0; i< array.length; i++){ 
            Promise.resolve(array[i]).then(
                 data => result[i] = data
                 if(++count === array.length){
                     resolve(result)
                 },
                 err => {
                    reject(err)
                    return
                }
            )
        }
      }
    })
}

Promise.race()

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。

const promise1 = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, 'one');
});

const promise2 = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, 'two');
});

Promise.race([promise1, promise2]).then((value) => {
  console.log(value);  // two
  // Both resolve, but promise2 is faster
});

手写Promise.race()方法

Promise.race = function(array){
  return new Promise((resolve,reject)=>{
    for(let i of array){
      Promise.resolve(i).then(
        data => resolve(data)
      ).catch(
        err => reject(err)
      )
    }
  })
}

Promise.any()

警告: Promise.any()方法依然是试验性的,尚未被所有的浏览器完全支持
该方法的入参也是一个包含Promise的数组,但Promise.any()Promise.all方法相反,当数组中有一个Promise的状态是成功它会立即返回成功的状态,只有当数组中所有的Promise都是失败状态时,它才会返回失败。

const pErr = new Promise((resolve, reject) => {
  reject("总是失败");
});

const pSlow = new Promise((resolve, reject) => {
  setTimeout(resolve, 500, "最终完成");
});

const pFast = new Promise((resolve, reject) => {
  setTimeout(resolve, 100, "很快完成");
});

Promise.any([pErr, pSlow, pFast]).then((value) => {
  console.log(value);
  // pFast fulfils first
})

Map、Set、WeakMap、WeakSet的区别

前言

  • SetMap主要用于数据去重和数据储存
  • Set是值的集合,Map则是以键值对的方式进行存储

Set

Set对象是值的集合,你可以按照插入的顺序迭代它的元素。Set 中的元素只会出现一次,即 Set 中的元素是唯一的。

由于Set中的元素只会出现一次,所以Set中的值总是唯一的,此外NaN undefined 都可以被存储在 Set 中,NaN 之间被视为相同的值(NaN 被认为是相同的,尽管 NaN !== NaN

工作中我们经常使用Set来对数据进行去重,举个例子

let set = new Set([1,1,2,2,3,3,4])
console.log(set)  // [1,2,3,4]

需要注意的是,向Set加入值时,是不会发生类型的转换的,所以5和'5'是两个不同的值,这和===的比较有些类似,不同的是NaN === NaN为false,而在Set中他认为NaN都是一个值

let set = new Set([5,'5'])
console.log(set)  // [5,'5']

let set2 = new Set([NaN,NaN])
console.log(set2) //  [NaN]

接下来让我们看看Set实例的属性和方法都有哪些

Set实例的属性

  • constructor 构造函数
  • size:元素的数量
let set = new Set([1,2,3,4,5])
console.log(set.size)  // 5  类似于数组的.length方法

Set实例的方法

  • add(value): 往Set中添加值,类似于数组的push操作
  • delete(value): 删除Set中的某个值
  • has(value): 判断Set中是否存在某个值, 返回值是boolean类型
  • clear(value): 清空Set数据
let set = new Set([])
set.add(1)
console.log(set) // [1]
set.has(1)  //  true
set.has(2)  // false
set.delete(1)  // []

Set默认可遍历,可以使用map、filter等方法,所以我们借助Set可以很容易实现数据的交集、并集、差集

let s1 = new Set([1,2,3])
let s2 = new Set([2,3,4])

let intersect = s1.filter(item => s2.has(item))   // 2,3
let union = new Set([...s1,...s2])  //  1,2,3,4
let difference = s1.filter(item => !s2.has(item))  //  1

WeakSet

WeakSet 对象允许你将弱保持对象存储在一个集合中
它和Set对象的主要区别有:

  • WeakSet只能是对象的集合,而不能像Set那样,是任何类型的任何值
  • WeakSet 持弱引用:集合中对象的引用为弱引用。如果没有其他的对WeakSet中对象的引用,那么这些对象会被当成垃圾回收掉,所以,WeakSet 对象里有多少个成员元素,取决于垃圾回收机制有没有运行,运行前后成员个数可能不一致,遍历结束之后,有的成员可能取不到了(被垃圾回收了),WeakSet 对象是无法被遍历的(ES6 规定 WeakSet不可遍历),也没有办法拿到它包含的所有元素

WeakSet实例的属性和方法

  • constructor 构造函数
  • add(value)
  • delete(value)
  • has(value)
let set = new WeakSet()
set.add(1)   // 报错  caught TypeError: Invalid value used in weak set
// 只能储存对象
set.add(window)
set.has(window)  // true
set.delete(window)
set.has(window)  // false

字典Map

集合 与 字典 的区别:

  • 共同点:集合、字典 可以储存不重复的值
  • 不同点:集合 是以 [value, value]的形式储存元素,字典 是以 [key, value] 的形式储存
let map = new Map()
let obj = { a: 1}
map.set(obj,'content')   // map添加值的方法是set而不是add
map.get(obj)  // content
map.get(o)  // undefined   读取一个未知的的键,返回的是undefined

map.delete(obj)
map.has(obj)  // false

需要注意的是,只有对同一对象的引用,Map才将它视为同一个键

let map = new Map()
map.set([1],'content')
map.get([1])   // undefined
map.has([1])   // false

上述例子中,虽然都是获取[1],但是该数组对象的引用地址不同,所以Map没有将其视为是同一个键

Map的属性及方法

属性

  • contructor 构造函数
  • size: 包含的元素个数

方法

  • set(key,value): 给字典添加新元素
  • get(key): 通过键查找特定的数值并返回
  • has(key): 判断字典中是否存在键key
  • delete(key): 通过键 key 从字典中移除对应的数据
  • clear(): 清空字典中数据

遍历方法

  • keys():将字典中包含的所有键名以迭代器形式返回
  • values():将字典中包含的所有数值以迭代器形式返回
  • entries():返回所有成员的迭代器
  • forEach():遍历字典的所有成员

WeakMap

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

要注意的是WeakMap弱引用的只是键名,而不是键值。键值依然是正常引用。
WeakMap 中,每个键对自己所引用对象的引用都是弱引用,在没有其他引用和该键引用同一对象,这个对象将会被垃圾回收(相应的key则变成无效的),所以,WeakMap 的 key 是不可枚举的

属性

  • contructor: 构造函数

方法

  • has(key):判断是否有 key 关联对象
  • get(key):返回key关联对象(没有则则返回 undefined)
  • set(key):设置一组key关联对象
  • delete(key):移除 key 的关联对象

总结

Set

  • 成员唯一,无序且不重复
  • 以[value,value]的形式存储,是值的集合,没有键名
  • 可以遍历

WeakSet

  • 成员都是对象
  • 成员都是弱引用,可以被垃圾回收机制回收,可以用来保存DOM节点,不容易造成内存泄漏
  • 不能遍历

Map

  • 以[key,value]的形式储存数据,是键值对的集合
  • 可以遍历

WeakMap

  • 只能接受对象作为键名,不接受其他类型作为键名
  • 键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的
  • 不能遍历

聊一聊bind、apply、call之间的区别

bind

bind()方法会创建一个新的函数,在bind()被调用时,新函数的this会指定为bind()传入的的第一个参数,剩余的参数将作为新函数的参数,供新函数调用时使用

语法

function.bind(thisArg,[arg1,arg2,...])

返回值

bind方法会返回一个原函数的拷贝,并拥有指定的this值和初始参数,也就是说使用bind方法不会立即执行原函数

示例

num = 99
const obj = {
  num:1,
  func:function(){
    return this.num
  }
}
let s1 = obj.func
console.log(s1());  // 99
console.log(obj.func());  // 1


let fn = obj.func
let newFn = fn.bind(obj)
console.log(newFn()); // 1

从上面的示例可以看出使用bind方法后,原函数fn的this指向了obj对象,所以返回的是1

apply

apply()方法调用一个具有给定this值的函数,以及一个数组(或者一个类数组对象)的形式提供参数

语法

apply(thisArg)
apply(thisArg, argsArray)
  • thisArg: 在func函数运行时使用的this值,需要注意的是this值可能不是该方法看到的实际值,在非严格模式下,指定null或者undefined时会自动转化为指向全局对象
  • argsArray: 一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 func 函数。如果该参数的值为nullundefined,则表示不需要传入任何参数

返回值

它会返回调用指定this值和参数的函数的结果,也就是说调用apply方法会立即执行原函数

示例

const array = ['a', 'b'];
const elements = [0, 1, 2];
array.push.apply(array, elements);
console.info(array); // ["a", "b", 0, 1, 2]

call

call()方法使用一个指定的this值和一个或者多个参数来调用函数

备注:该方法的作用与apply方法类似,不同是传参形式的区别,call()方法接受的是一个参数列表,而apply()接受的是一个包含多个参数的数组

语法

function.call(thisArg, arg1, arg2, ...)

返回值

使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined。也就是说使用call方法也会立即执行原函数

示例

function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}

var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);

一般我们可以使用call方法来实现对象的继承,例如通过构造函数实现继承

总结

  • bind函数改变this指向不直接调用,call和apply改变this指向直接调用
  • apply接受的第二参数为一个数组
  • call用于对象的继承、伪数组转成真数组
  • apply用于找出数组中的最大值和最小值以及数组合并等
  • bind用于Vue或者React框架中改变函数运行时的this指向

new 关键字都做了什么事?

new 定义

new在mdn中的定义:

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
光看定义的话可能有点难以理解,我们可以先来看看new关键字都帮我做了哪些事

function Father(name,age){
    this.name = name
    this.age = age
}
Father.prototype.sayHello = function (){
    console.log('hello world')
}
let s1 = new Father('jony',25)
console.log(s1.name)  // jony
console.log(s1.age)  // 25
console.log(s1.sayHello) // hello world

从上述例子中,我们可以看出经过new操作后,s1实例可以

  • 访问Father构造函数中的属性和方法
  • 访问Father原型上的属性和方法

模拟实现

其实在使用new关键字后,他会帮我们完成以下操作

  • 1、创建一个空的简单 JavaScript 对象(即 {})
  • 为步骤 1 新创建的对象添加属性 proto,将该属性链接至构造函数的原型对象
  • 将步骤 1 新创建的对象作为 this 的上下文
  • 如果该函数没有返回对象,则返回 this

我们可以通过自定义函数模拟一下new关键字的操作

function newObject(){
    let obj = new Object()
    Constructor = [].shift.call(arguments)  // 去除arguments中的第一个参数
    obj.__proto__ = Constructor.prototype
    Constructor.apply(obj,arguments)  //执行构造函数,给obj对象添加上属性和方法
    return obj
}

上述自定义函数newObject是基于函数没有返回对象实现的,假设函数有返回对象

function Father(name,age){
    this.name = name
    this.age = age
    return {
        width:99
    }
}
let s1 = new Father('jony',25)
console.log(s1.name)  // undefined
console.log(s1.age)  // undefined
console.log(s1.width) //  99

可以看出如果构造函数返回了一个对象,实例s1就只能访问返回的对象中的属性和方法
还要注意的是,如果构造函数返回的不是一个对象,而是一个值类型时

function Father(name,age){
    this.name = name
    this.age = age
    return 'hello world'
}
let s1 = new Father('jony',25)
console.log(s1.name)  // jony
console.log(s1.age)  // 25
console.log(s1.width) // undefined

如果构造函数的返回值是一个值类型,就相当于没有返回值来处理

基于以上两种情况,我们再对newObject函数进行下改造

function newObject(){
    let obj = new Object()
    Constructor = [].shift.call(arguments)
    obj.__proto__ = Constructor.prototype
    const result = Constructor.apply(obj,arguments)
    return typeof result == 'object' ? result : obj   //判断构造函数返回值的类型,如果是对象,则return这个返回值,如果是值类型就返回创建的对象obj
}

手写一个js控制并发数的函数?

手写代码

function control(limit, requestList) {
  return new Promise(async (resolve) => {
    let length = requestList.length // 请求数组的长度
    let pool = [] // 请求池
    let result = [] // 所有的结果
    let count = 0 // 已完成的数量
    for (let i = 0; i < length; i++) {
      let task = requestList[i]
      const handler = (res) => {
        pool.splice(pool.indexOf(task), 1)
        result[i] = res
        count++
        if (count === length) {
          resolve(result)
        }
      }
      task.then(
        res => handler(res),
        err => handler(err)
      )
      pool.push(task)
      if (pool.length >= limit) {
        await Promise.race(pool)
      }
    }
  })
}

执行看看

let stack = [
  new Promise((resolve)=>{
    resolve(1)
  }),
  new Promise((resolve)=>{
    resolve(2)
  }),
  new Promise((resolve)=>{
    resolve(3)
  }),
  new Promise((resolve)=>{
    resolve(4)
  }),
  new Promise((resolve)=>{
    resolve(5)
  }),
  new Promise((resolve)=>{
    resolve(6)
  }),
  new Promise((resolve)=>{
    resolve(7)
  })
]
control2(2,stack).then(res => console.log(res,'数据'))
//打印结果应该是

深入学习BFC块格式化上下文

一、什么是BFC

块格式化上下文(Block Formatting Context,简称BFC)是 Web 页面的可视 CSS 渲染的一部分,是块级盒子的布局过程发生的区域,也是浮动元素与其他元素交互的区域。通俗的讲,BFC就是一个容器,用于管理块级元素

二、如何创建BFC

  • 根元素()
  • 浮动元素(float值不为 none)
  • 绝对定位元素(position为absolute或者fixed)
  • display为 table-cell|table-caption|inline-block|inline-flex|flex
  • overflow不为visible或者clip的块元素,可以是hidden|auto|scroll

三、BFC的布局规则

  • 在同一个BFC下相邻两个块级元素margin会发生重叠,margin重叠的三个条件(同属于一个BFC、相邻、块级元素)
  • 内部div会在垂直方向上一个接一个的排列
  • BFC区域不会于float元素发生重叠(可以实现自适应两栏布局)
  • 计算BFC的高度时,浮动元素也参与计算。
  • 区域内元素的布局不会影响区域外的元素,反之亦然

四、BFC有哪些特性

特性1、BFC会阻止垂直外边距折叠

只有同属于一个BFC时,两个元素才有可能发生垂直margin的重叠,这个包括相邻元素或者嵌套元素,只要他们之间没有阻挡(比如边框、非空内容、padding等)就会发生margin重叠。

  • 相邻两个元素margin重叠
<style>
p{
        color: #fff;
        background: #888;
        width: 200px;
        line-height: 100px;
        text-align:center;
        margin: 100px;
  }
</style>
<body>
    <p>ABC</p>
    <p>abc</p>
</body>

相邻元素margin重叠问题
上面两个元素的距离应该有200px,但实际上只有100px,这里发生了margin的重叠,那么如何解决呢,其实很简单,只需给其中的一个p元素包裹一个div,他让形成一个新的BFC区域

<style>
p{
        color: #fff;
        background: #888;
        width: 200px;
        line-height: 100px;
        text-align:center;
        margin: 100px;
  }
div{
      overflow:hidden
}
</style>
<body>
    <p>ABC</p>
    <div>
         <p>abc</p>
    </div>
</body>

完善后

  • 父子元素发生垂直方向margin重叠
<style>
.box{
width:100px;
height:100px;
background:#ccc;
}
.wrap {
  background:yellow;
}
.wrap h1{
  background:pink;
  margin:40px;
}
</style>
<body>
<div class="box">box</div>
<div class="wrap">
  <h1>h1</h1>
</div>
</body>

父子元素margin重叠
上述例子中,wrap元素与h1元素应该有上下40px的margin,实际上没有,反而box与wrap元素有了40px的距离,这种情况的解决方法和之前的一样,也是让wrap形成一个新的BFC区域就能完美解决啦

<style>
.box{
width:100px;
height:100px;
background:#ccc;
}
.wrap {
  background:yellow;
  overflow:hidden
}
.wrap h1{
  background:pink;
  margin:40px;
}
</style>
<body>
<div class="box">box</div>
<div class="wrap">
  <h1>h1</h1>
</div>
</body>

完善后

特性2:BFC不会重叠浮动元素

利用这个特性,我们可以创造自适应两栏布局。

<style>
.box1{
  height: 100px;
  width: 100px;
  float: left;
  background: lightblue;
}
.box2{
  overflow:hidden;
  height: 200px;
  background: #eee;
}
</style>
<body>
<div class="box1">我是一个左浮动的元素</div>
<div class="box2">喂喂喂!大家不要生气嘛,生气会犯嗔戒的。悟空你也太调皮了,
我跟你说过叫你不要乱扔东西,你怎么又……你看,我还没说完你就把棍子给扔掉了!
月光宝盒是宝物,你把它扔掉会污染环境,要是砸到小朋友怎么办,就算砸不到小朋友,
砸到花花草草也是不对的。</div>
</body>

让box2形成BFC区域,就能实现自适应两栏布局
自适应两栏布局

特性3:BFC可以包含浮动----清除浮动

<style>
.box1{
  width:100px;
  height:100px;
  float:left;
  border: 1px solid #000;
}
.box2{
  width:100px;
  height:100px;
  float:left;
  border: 1px solid #000;
}
.box{
  background:yellow
}
</style>
<body>
<div class="box">
  <div class="box1"></div>
  <div class="box2"></div>
</div> 
</body>

高度塌陷
这里父元素包裹了两个浮动元素,没有设置高度,于是发生了高度塌陷,未能将子元素包裹住,解决这个问题也很简单,让父元素形成BFC区域,BFC区域会计算浮动元素的高度;
给父元素box加上overflow:hidden,形成BFC区域
完善后

javascript中Number数据类型的浮点数精度

前言

0.1 + 0.2 == 0.3 ?,没有学编程的人看到这道算术题,都会认为这个等式肯定成立呀,难不成又是一个脑经急转弯?嘿嘿,先别急,在javascript这门语言中,0.1 + 0.2 == 0.3 这个等式式不成立的,这是由于Number数据类型的浮点数精度导致的

Number数据类型

在 IEEE754 中,规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度、与延伸双精确度。Number类型是一种基于IEEE754标准的双精度64位二进制格式值,也就是采用双精确度,用64位来储存一个浮点数。

浮点数转二进制及存储

十进制小数转换成二进制小数采用乘2取整,顺序排列法。具体做法是:用2乘十进制小数,可以得到积,将积的整数部分取出,再用2乘余下的小数部分,又得到一个积,再将积的整数部分取出,如此进行,直到积中的小数部分为零,或者达到所要求的精度为止,拿0.1转成二进制为例:

0.1 * 2 = 0.2  //取0
0.2 * 2 = 0.4  //取0
0.4 * 2 = 0.8  //取0
0.8 * 2 = 1.6  //取1
0.6 * 2 = 1.2  //取1
0.2 * 2 = 0.4 //取0
......

可以看出0.1转成二进制的运算过程中,乘积中小数部分永远不会为0,会一直循环下去,直到到达ECMAScript规定的64位来进行存储

Number(0.1).toString(2) // 0.0001100110011001100110011001100110011001100110011001101
Number(0.2).toString(2) // 0.001100110011001100110011001100110011001100110011001101

所以我们看出0.1在转换成二进制并存储的过程中,精度就已经丢失了,当我使用浮点数进行运算时,也是使用已经丢失精度的数据进行运算

浮点数的运算

关于浮点数的运算,一般由以下五个步骤完成:对阶、尾数运算、规格化、舍入处理、溢出判断。我们来简单看一下 0.1 和 0.2 的计算。

首先是对阶,所谓对阶,就是把阶码调整为相同,比如 0.1 是 1.1001100110011…… * 2^-4,阶码是 -4,而 0.2 就是 1.10011001100110...* 2^-3,阶码是 -3,两个阶码不同,所以先调整为相同的阶码再进行计算,调整原则是小阶对大阶,也就是 0.1 的 -4 调整为 -3,对应变成 0.11001100110011…… * 2^-3

接下来是尾数计算:

  0.1100110011001100110011001100110011001100110011001101
+ 1.1001100110011001100110011001100110011001100110011010
————————————————————————————————————————————————————————
 10.0110011001100110011001100110011001100110011001100111

我们得到结果为10.0110011001100110011001100110011001100110011001100111 * 2^-3

将这个结果处理一下,即结果规格化,变成1.0011001100110011001100110011001100110011001100110011(1) * 2^-2

括号里的 1 意思是说计算后这个 1 超出了范围,所以要被舍弃了。

再然后是舍入,四舍五入对应到二进制中,就是 0 舍 1 入,因为我们要把括号里的 1 丢了,所以这里会进一,结果变成

1.0011001100110011001100110011001100110011001100110100 * 2^-2

本来还有一个溢出判断,因为这里不涉及,就不讲了。

所以最终的结果存成 64 位就是

0 01111111101 0011001100110011001100110011001100110011001100110100

将它转换为10进制数就得到 0.30000000000000004440892098500626

因为两次存储时的精度丢失加上一次运算时的精度丢失,最终导致了 0.1 + 0.2 !== 0.3

进制转换的方法

num.toString(2);        // 十进制转二进制
num.toString(8);        // 十进制转八进制
num.toString(10);      // 十进制转十进制
num.toString(16);      // 十进制转十六进制

parseInt(num, 2);     // 二进制转十进制;num 被看做是二进制的数
parseInt(num, 8);     // 八进制转十进制;num 被看做是八进制的数
parseInt(num, 16);   // 十六进制转十进制;num 被看做是十六进制的数

parseInt(num, 2).toString(8);     // 二进制转八进制;也可以看做是二进制先转成十进制,再转成八进制
parseInt(num, 2).toString(16);    // 二进制转十六进制;也可以看做是二进制先转成十进制,再转成十六进制

// 以指定的精度返回该数值对象的字符串表示
(0.1 + 0.2).toPrecision(21)
=> "0.300000000000000044409"
(0.3).toPrecision(21)
=> "0.299999999999999988898"

Vuex持久化储存和$route对象序列号报错

由于Vuex在浏览器刷新是会丢失数据,所以我在项目里使用了Vuex持久化存储的功能,我这边实现的方案是监听页面刷新的事件,将Vuex中的数据存到sessionStorage中,在页面初始化的时候再将sessionStorage中的数据设进Vuex中,具体代码如下

create(){
      //在页面加载时读取sessionStorage里的状态信息
    if (sessionStorage.getItem("store")) {
      this.$store.replaceState(
        Object.assign(
          {},
          this.$store.state,
          JSON.parse(sessionStorage.getItem("store"))
        )
      );
       sessionStorage.removeItem("store");
    }

    //在页面刷新时将vuex里的信息保存到sessionStorage里
    window.addEventListener("beforeunload", () => {
      sessionStorage.setItem("store", JSON.stringify(this.$store.state));
    });
}

这样一个简单的Vuex持久化储存的功能就实现了,浏览器刷新后我们也能读取到Vuex中储存的数据,但是突然有一天,这个功能失效了,页面也没有报错,经过我的一番摸索,最终将问题锁定在了Vuex中储存的$route对象上

$Route路由对象属性

$Route对象上有以下这几个属性

  • path:字符串,对应当前路由的路径,总是解析为绝对路径,如 "/foo/bar"
  • params:一个 key/value 对象,包含了动态片段和全匹配片段,如果没有路由参数,就是一个空对象
  • query:一个 key/value 对象,表示 URL 查询参数。例如,对于路径 /foo?user=1,则有 $route.query.user == 1,如果没有查询参数,则是个空对象
  • hash:当前路由的 hash 值 (带 #) ,如果没有 hash 值,则为空字符串
  • fullPath:完成解析后的 URL,包含查询参数和 hash 的完整路径
  • matched:一个数组,包含当前路由的所有嵌套路径片段的路由记录 。路由记录就是 routes 配置数组中的对象副本 (还有在 children 数组)

问题就出在了$route这个路由对象上,我们在页面刷新时要将Vuex中的数据转换成字符串,再进行储存

    //在页面刷新时将vuex里的信息保存到sessionStorage里
    window.addEventListener("beforeunload", () => {
      sessionStorage.setItem("store", JSON.stringify(this.$store.state));
    });

而store中如果有$route就会报错,这是因为$route包含了一些非序列话的属性,例如函数、循环引用等;

当使用JSON.stringify($route)时,可能会报错TypeError: Converting circular structure to JSON,这是因为$route对象包含循环引用,即对象内部存在相互引用,导致JSON.stringify()无法序列化这个对象,从而抛出这个错误。

解决方法

为了解决这个问题,你可以将$route对象转换成一个新的纯JavaScript对象,然后再进行序列化。可以使用Object.assign()方法来实现这一点,如下所示:

JSON.stringify(Object.assign({}, $route))

这个方法会创建一个新的JavaScript对象,将$route的属性复制到这个新对象中,并返回这个新对象。由于这个新对象只包含纯JavaScript数据,因此可以安全地序列化它。

通过这次分享加深下对知识点的理解,也希望对大家有些帮助,希望能够多多点赞支持下~

手写一个发布订阅模式

前言

发布订阅是最常见的设计模式之一,像Vue使用的$bus事件总线就是采用了发布订阅的模式来实现组件之间的消息传递

我们可以把发布-订阅模式看成一个微信公众号,公众号属于发布者,用户属于订阅者,用户将订阅公众号的事件注册到调度中心,公众号作为发布者,当有新文章发布时,公众号发布该事件到调度中心,调度中心会及时发消息告知用户。

这个“微信公众号”主要有四个方法

  • on(type, cb): 订阅事件,但是事件不会执行
  • emit(type, ...args) : 发布消息,执行订阅的事件
  • once(type, cb): 表示只订阅一次事件,发布完成后直接删除该事件
  • off(type, cb): 取消订阅,删除事件

简单代码实现

class Bus {
     constructor(){
         this.event = {}
     }

     emit(type,...args){
        if(!this.event[type]){
        return
    }
    else{
        this.event[type].forEach(cb => cb(...args))
      }
   }

   on(type,cb){
     if(!this.event[type]){
       this.event[type] = [cb]
     }
     else{
       this.event[type].push(cb)
     }
   }

   off(type,cb){
    if(!this.event[type]){
      return
    }
    else{
      this.event[type] = this.event[type].filter(item => item !== cb)
    }
   }
   once(type,cb){
     const fn = () =>{
       cb()
       this.off(type,cb)
     }
     this.on(type,fn)
   }
}

let bus = new Bus()
let func = (str)=>{
  console.log(str);
}
bus.on('emit',func)
bus.emit('emit',123)
bus.once('emit',func)
bus.off('emit', func)

圣杯布局和双飞翼布局

前言

在CSS布局中,三栏式布局是我们最常用的布局方式,其中圣杯布局和双飞翼布局是三栏式布局当中的佼佼者,我们一起来认识一下

圣杯布局

<!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>圣杯布局</title>
  <style>
    body{
      padding: 0;
      margin: 0;
    }
    header,footer{
      height: 100px;
      width: 100%;
      background-color: lightskyblue;
    }
    .container{
      height: 300px;
      padding-left: 200px;
      padding-right:300px
    }
    .container div{
      float: left;
       /* 圣杯布局 */
      position: relative;
    }
    .main{
      width: 100%;
      background-color: pink;
      height: 300px;
    }
    .left{
      width: 200px;
      height: 300px;
      background-color: lightgreen;
      margin-left: -100%;
       /* 圣杯布局 */
      left: -200px;
      
    }
    .right{
      width: 300px;
      height: 300px;
      background-color: #666;
      margin-left: -300px;
       /* 圣杯布局 */
      right: -300px;
      
    }
  </style>
</head>
<body>
  <header>头部</header>
  <div class="container">
    <div class="main">中间</div>
    <div class="left">左边</div>
    <div class="right">右边</div>
  </div>
  <footer>页脚</footer>
</body>
</html>

双飞翼布局

<!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>双飞翼布局</title>
  <style>
    body{
      padding: 0;
      margin: 0;
    }
    header,footer{
      height: 100px;
      width: 100%;
      background-color: lightskyblue;
    }
    .container{
      height: 300px;
    }
    .container div{
      float: left;
    }
    .middle{
      width: 100%;
      background-color: pink;
      height: 300px;
    }
    /* 双飞翼布局 */
    .main{
      margin-left: 200px;
      margin-right: 300px;
    }
    .left{
      width: 200px;
      height: 300px;
      background-color: lightgreen;
      margin-left: -100%;
      
    }
    .right{
      width: 300px;
      height: 300px;
      background-color: #666;
      margin-left: -300px;
      
    }
  </style>
</head>
<body>
  <header>头部</header>
  <div class="container">
    <div class="middle">
      <div class="main">中间</div>
    </div>
    <div class="left">左边</div>
    <div class="right">右边</div>
  </div>
  <footer>页脚</footer>
</body>
</html>

总结

一般页面最主要的内容都是放在main中进行渲染,为保证用户能第一时间看到页面最主要的内容,所以诞生出圣杯布局和双飞翼布局,这也是为什么main要放在最上面的原因;

圣杯布局和双飞翼布局的不同点:

  • 圣杯布局采用的是padding + relative的方式来实现
  • 双飞翼布局则是采用margin的方式来实现
  • 圣杯布局相较于双飞翼布局,样式设置稍有些复杂。双飞翼布局样式设置比较简单,但需要修改html布局结构

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.