CobraJ

CobraJ 查看完整档案

北京编辑北京邮电大学网络教育学院  |  计算机科学 编辑某通信服务公司  |  前端工程师 编辑 none.none 编辑
编辑

take it easy , relax

个人动态

CobraJ 收藏了文章 · 4月7日

JS 装饰器,一篇就够

更多文章,请在Github blog查看

更新 2020.09.23

最近一直没关注 decorators 的提案,居然才知道新的提案都发布了两年了,而且和旧提案不兼容,几乎算是重写了。

Babel 7.1.0 最终支持了新的装饰器提案:你可以使用 @babel/plugin-proposal-decorators 插件来提前尝试此功能 🎉。

相关历史

三年多以前,Yehuda Katz 首先提出了装饰器的概念。TypeScript 在 1.5 版本(2015)中发布了对装饰器的支持以及许多 ES6 的相关特性。 一些主流框架,如 Angular 和 MobX 等开始使用它们来增加开发者体验:这使得装饰器非常受欢迎,并给社区带来了一种已经稳定的错觉。

Babel 第一次实现装饰器是在 v5 版本中,但由于该提案仍在不断变化,则在 Babel v6 中移除了它们。Logan Smyth 创建了一个非官方的插件(babel-plugin-transform-decorators-legacy),它延用了 Babel 5 中装饰器的行为;在 Babel 7 的 alpha 版本发布期间该库被移至 Babel 官方的仓库中。当时该插件仍使用旧的装饰器语法,因为新提案尚未明确。

自那时起,Daniel Ehrenberg、Brian Terlson 以及 Yehuda Katz 就一起成为了该提案的共同作者,该提案几乎已被完全重写。当然并非一切事情都已确定,因为至今尚未出现符合规范的实现方式。

Babel 7.0.0 为 @babel/plugin-proposal-decorators 插件引入了新的标识:legacy 选项,其唯一有效值为 true。这种突破性变更是必要的,它为提案从第一阶段到当前阶段平稳过渡作铺垫。

在 Babel 7.1.0 中,我们引入了对这个新提案的支持,并且当 @babel/plugin-proposal-decorators 插件被使用时,默认启用。而在 Babel 7.0.0 中如果我们不设置 legacy: true 选项,默认情况下就不能使用该语义(相当于 legacy: false)。

新提案同时支持使用装饰器实现私有字段(private fields)和私有方法(private methods)。我们尚未在 Babel 中实现此功能(在每个 class 中使用装饰器或私有元素),但我们会很快去出现它。

装饰器函数相关参数

新提案提出的第三个重要变化与传递给装饰器函数参数相关。

在提案的第一个版本中,类元素装饰器接收的参数分别为目标类(或对象),key 以及属性描述符 - 与传递给 Object.defineProperty 的形式类似。类装饰器将目标构造函数(constructor)作为唯一参数。

新的装饰器提案更加强大:元素装饰器会接收一个对象,该对象除更改属性描述符外,还允许更改 key 值,可以赋值(static,prototype 或者 own),以及元素的类型(field 或 method)。它们还可以创建其他属性并在装饰类上定义运行函数(完成器)

类装饰器接收一个包含类描述符的对象,使得类在创建之前修改它们成为可能。

参考


* * * 分割线 * * *


在 ES6 中增加了对类对象的相关定义和操作(比如 class 和 extends ),这就使得我们在多个不同类之间共享或者扩展一些方法或者行为的时候,变得并不是那么优雅。这个时候,我们就需要一种更优雅的方法来帮助我们完成这些事情。

什么是装饰器

Python 的装饰器

在面向对象(OOP)的设计模式中,decorator被称为装饰模式。OOP的装饰模式需要通过继承和组合来实现,而Python除了能支持 OOP 的 decorator 外,直接从语法层次支持 decorator。

如果你熟悉 python 的话,对它一定不会陌生。那么我们先来看一下 python 里的装饰器是什么样子的吧:

def decorator(f):
    print "my decorator"
    return f
@decorator
def myfunc():
    print "my function"
myfunc()
# my decorator
# my function

这里的 @decorator 就是我们说的装饰器。在上面的代码中,我们利用装饰器给我们的目标方法执行前打印出了一行文本,并且并没有对原方法做任何的修改。代码基本等同于:

def decorator(f):
    def wrapper():
        print "my decorator"
        return f()
    return wrapper
def myfunc():
    print "my function"
myfunc = decorator(myfuc)

通过代码我们也不难看出,装饰器 decorator 接收一个参数,也就是我们被装饰的目标方法,处理完扩展的内容以后再返回一个方法,供以后调用,同时也失去了对原方法对象的访问。当我们对某个应用了装饰以后,其实就改变了被装饰方法的入口引用,使其重新指向了装饰器返回的方法的入口点,从而来实现我们对原函数的扩展、修改等操作。

ES7 的装饰器

ES7 中的 decorator 同样借鉴了这个语法糖,不过依赖于 ES5 的 Object.defineProperty 方法 。

Object.defineProperty

Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

该方法允许精确添加或修改对象的属性。通过赋值来添加的普通属性会创建在属性枚举期间显示的属性(for...in 或 Object.keys 方法), 这些值可以被改变,也可以被删除。这种方法允许这些额外的细节从默认值改变。默认情况下,使用 Object.defineProperty() 添加的属性值是不可变的。

语法

Object.defineProperty(obj, prop, descriptor)
  • obj:要在其上定义属性的对象。
  • prop:要定义或修改的属性的名称。
  • descriptor:将被定义或修改的属性描述符。
  • 返回值:被传递给函数的对象。

在ES6中,由于 Symbol类型 的特殊性,用 Symbol类型 的值来做对象的key与常规的定义或修改不同,而Object.defineProperty 是定义 key为 Symbol 的属性的方法之一。

属性描述符

对象里目前存在的属性描述符有两种主要形式:数据描述符存取描述符

  • 数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。
  • 存取描述符是由 getter-setter 函数对描述的属性。

描述符必须是这两种形式之一;不能同时是两者。

数据描述符和存取描述符均具有以下可选键值:

configurable

当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。

enumerable

enumerable定义了对象的属性是否可以在 for...in 循环和 Object.keys() 中被枚举。

当且仅当该属性的 enumerable 为 true 时,该属性才能够出现在对象的枚举属性中。默认为 false。
数据描述符同时具有以下可选键值:

value

该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。

writable

当且仅当该属性的 writable 为 true 时,value 才能被赋值运算符改变。默认为 false。

存取描述符同时具有以下可选键值:

get

一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。该方法返回值被用作属性值。默认为 undefined。

set

一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。该方法将接受唯一参数,并将该参数的新值分配给该属性。默认为 undefined。

如果一个描述符不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常。

用法

类的装饰

@testable
class MyTestableClass {
  // ...
}

function testable(target) {
  target.isTestable = true;
}

MyTestableClass.isTestable // true

上面代码中,@testable 就是一个装饰器。它修改了 MyTestableClass这 个类的行为,为它加上了静态属性isTestable。testable 函数的参数 target 是 MyTestableClass 类本身。

基本上,装饰器的行为就是下面这样。

@decorator
class A {}

// 等同于

class A {}
A = decorator(A) || A;

也就是说,装饰器是一个对类进行处理的函数。装饰器函数的第一个参数,就是所要装饰的目标类

如果觉得一个参数不够用,可以在装饰器外面再封装一层函数。

function testable(isTestable) {
  return function(target) {
    target.isTestable = isTestable;
  }
}

@testable(true)
class MyTestableClass {}
MyTestableClass.isTestable // true

@testable(false)
class MyClass {}
MyClass.isTestable // false

上面代码中,装饰器 testable 可以接受参数,这就等于可以修改装饰器的行为。

注意,装饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,装饰器能在编译阶段运行代码。也就是说,装饰器本质就是编译时执行的函数

前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的 prototype 对象操作。

下面是另外一个例子。

// mixins.js
export function mixins(...list) {
  return function (target) {
    Object.assign(target.prototype, ...list)
  }
}

// main.js
import { mixins } from './mixins'

const Foo = {
  foo() { console.log('foo') }
};

@mixins(Foo)
class MyClass {}

let obj = new MyClass();
obj.foo() // 'foo'

上面代码通过装饰器 mixins,把Foo对象的方法添加到了 MyClass 的实例上面。

方法的装饰

装饰器不仅可以装饰类,还可以装饰类的属性。

class Person {
  @readonly
  name() { return `${this.first} ${this.last}` }
}

上面代码中,装饰器 readonly 用来装饰“类”的name方法。

装饰器函数 readonly 一共可以接受三个参数。

function readonly(target, name, descriptor){
  // descriptor对象原来的值如下
  // {
  //   value: specifiedFunction,
  //   enumerable: false,
  //   configurable: true,
  //   writable: true
  // };
  descriptor.writable = false;
  return descriptor;
}

readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);
  • 装饰器第一个参数是 类的原型对象,上例是 Person.prototype,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target参数指的是类本身);
  • 第二个参数是 所要装饰的属性名
  • 第三个参数是 该属性的描述对象

另外,上面代码说明,装饰器(readonly)会修改属性的 描述对象(descriptor),然后被修改的描述对象再用来定义属性。

函数方法的装饰

装饰器只能用于类和类的方法,不能用于函数,因为存在函数提升

另一方面,如果一定要装饰函数,可以采用高阶函数的形式直接执行。

function doSomething(name) {
  console.log('Hello, ' + name);
}

function loggingDecorator(wrapped) {
  return function() {
    console.log('Starting');
    const result = wrapped.apply(this, arguments);
    console.log('Finished');
    return result;
  }
}

const wrapped = loggingDecorator(doSomething);

core-decorators.js

core-decorators.js是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器。

@autobind

autobind 装饰器使得方法中的this对象,绑定原始对象。

@readonly

readonly 装饰器使得属性或方法不可写。

@override

override 装饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。

import { override } from 'core-decorators';

class Parent {
  speak(first, second) {}
}

class Child extends Parent {
  @override
  speak() {}
  // SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
}

// or

class Child extends Parent {
  @override
  speaks() {}
  // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
  //
  //   Did you mean "speak"?
}

@deprecate (别名@deprecated)

deprecate 或 deprecated 装饰器在控制台显示一条警告,表示该方法将废除。

import { deprecate } from 'core-decorators';

class Person {
  @deprecate
  facepalm() {}

  @deprecate('We stopped facepalming')
  facepalmHard() {}

  @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' })
  facepalmHarder() {}
}

let person = new Person();

person.facepalm();
// DEPRECATION Person#facepalm: This function will be removed in future versions.

person.facepalmHard();
// DEPRECATION Person#facepalmHard: We stopped facepalming

person.facepalmHarder();
// DEPRECATION Person#facepalmHarder: We stopped facepalming
//
//     See http://knowyourmeme.com/memes/facepalm for more details.
//

@suppressWarnings

suppressWarnings 装饰器抑制 deprecated 装饰器导致的 console.warn() 调用。但是,异步代码发出的调用除外。

使用场景

装饰器有注释的作用

@testable
class Person {
  @readonly
  @nonenumerable
  name() { return `${this.first} ${this.last}` }
}

从上面代码中,我们一眼就能看出,Person类是可测试的,而name方法是只读和不可枚举的。

React 的 connect

实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。

class MyReactComponent extends React.Component {}

export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);

有了装饰器,就可以改写上面的代码。装饰

@connect(mapStateToProps, mapDispatchToProps)
export default class MyReactComponent extends React.Component {}

相对来说,后一种写法看上去更容易理解。

新功能提醒或权限

菜单点击时,进行事件拦截,若该菜单有新功能更新,则弹窗显示。

/**
 * @description 在点击时,如果有新功能提醒,则弹窗显示
 * @param code 新功能的code
 * @returns {function(*, *, *)}
 */
 const checkRecommandFunc = (code) => (target, property, descriptor) => {
    let desF = descriptor.value; 
    descriptor.value = function (...args) {
      let recommandFuncModalData = SYSTEM.recommandFuncCodeMap[code];

      if (recommandFuncModalData && recommandFuncModalData.id) {
        setTimeout(() => {
          this.props.dispatch({type: 'global/setRecommandFuncModalData', recommandFuncModalData});
        }, 1000);
      }
      desF.apply(this, args);
    };
    return descriptor;
  };

loading

在 React 项目中,我们可能需要在向后台请求数据时,页面出现 loading 动画。这个时候,你就可以使用装饰器,优雅地实现功能。

@autobind
@loadingWrap(true)
async handleSelect(params) {
  await this.props.dispatch({
    type: 'product_list/setQuerypParams',
    querypParams: params
  });
}

loadingWrap 函数如下:

export function loadingWrap(needHide) {

  const defaultLoading = (
    <div className="toast-loading">
      <Loading className="loading-icon"/>
      <div>加载中...</div>
    </div>
  );

  return function (target, property, descriptor) {
    const raw = descriptor.value;
    
    descriptor.value = function (...args) {
      Toast.info(text || defaultLoading, 0, null, true);
      const res = raw.apply(this, args);
      
      if (needHide) {
        if (get('finally')(res)) {
          res.finally(() => {
            Toast.hide();
          });
        } else {
          Toast.hide();
        }
      }
    };
    return descriptor;
  };
}

问题:这里大家可以想想看,如果我们不希望每次请求数据时都出现 loading,而是要求只要后台请求时间大于 300ms 时,才显示loading,这里需要怎么改?

参考

查看原文

CobraJ 回答了问题 · 3月22日

为什么这样配webpack的devserver没有效果?

你这个写法, overrideDevServer 会传递config进入到 passConfig 吗?好好考虑一下

关注 2 回答 1

CobraJ 回答了问题 · 3月22日

解决Typescript中多类型判断该怎么写?

image.png
当然我例子是在同一个位置写的啊,你的interface肯定是写在d.ts的声明里面的,然后testA这几个就是写在你要判断的位置,然后你看看能不能帮你识别出来深层以后这个类型。希望能帮到你吧,也不是很确定,不过感觉不太会出现追不到类型的情况

关注 3 回答 3

CobraJ 回答了问题 · 3月15日

父组件如何获取子组件上的非prop属性

需要区分的select 加个自定义属性 :xxxx = "(sign = 'JScom')"
data 里面 加上sign ,然后watch :sign,然后你就能收到不同的create的时间节点的赋值,就能区分了

关注 4 回答 3

CobraJ 回答了问题 · 3月12日

vuetify v-checkbox for循环怎么选中其中一个,其他都选中?

首先checked。headers是数组吗?然后value的item。id是重复的吗

关注 2 回答 1

CobraJ 收藏了文章 · 3月8日

如何用 JS 实现二叉堆

这是第 90 篇不掺水的原创,想获取更多原创好文,请搜索公众号关注我们吧~ 本文首发于政采云前端博客:如何用 JS 实现二叉堆

如何用 JS 实现二叉堆

前言

二叉树(Binary Tree)是一种树形结构,它的特点是每个节点最多只有两个分支节点,一棵二叉树通常由根节点、分支节点、叶子节点组成,如下图所示。每个分支节点也常常被称作为一棵子树,而二叉堆是一种特殊的树,它属于完全二叉树。

二叉树与二叉堆的关系

在日常工作中会遇到很多数组的操作,比如排序等。那么理解二叉堆的实现对以后的开发效率会有所提升,下面就简单介绍一下什么是二叉树,什么是二叉堆。

二叉树特征

  • 根节点:二叉树最顶层的节点
  • 分支节点:除了根节点以外且拥有叶子节点
  • 叶子节点:除了自身,没有其他子节点

在二叉树中,我们常常还会用父节点和子节点来描述,比如上图中左侧节点 2 为 6 和 3 的父节点,反之 6 和 3 是 2 子节点。

二叉树分类

二叉树分为满二叉树(full binary tree)和完全二叉树(complete binary tree)。

  • 满二叉树:一棵深度为 k 且有 2 ^ k - 1个节点的二叉树称为满二叉树
  • 完全二叉树:完全二叉树是指最后一层左边是满的,右边可能满也可能不满,然后其余层都是满的二叉树称为完全二叉树(满二叉树也是一种完全二叉树)

二叉树结构

从图中我们可以看出二叉树是从上到下依次排列下来,可想而知可以用一个数组来表示二叉树的结构,从下标 index( 0 - 8 ) 从上到下依次排列。

3

  • 二叉树左侧节点表达式 index * 2 + 1。例如:以根节点为例求左侧节点,根节点的下标为0,则左侧节点的序数是1 ,对应数组中的值为1
  • 二叉树右侧节点表达式 index * 2 + 2。例如:以根节点为例求右侧节点,根节点的下标为0,则右侧节点的序数是2 ,对应数组中的值为 8
  • 二叉树叶子节点表达式 序数 >= floor( N / 2 )都是叶子节点(N是数组的长度)。例如:floor( 9 / 2 ) = 4 ,则从下标 4 开始的值都为叶子节点

二叉堆特征

二叉堆是一个完全二叉树,父节点与子节点要保持固定的序关系,并且每个节点的左子树和右子树都是一个二叉堆。

4

从上图可以看出

  • 图一:每个父节点大于子节点或等于子节点,满足二叉堆的性质
  • 图二:其中有一个父节点小于子节点则不满足二叉堆性质

二叉堆分类

​ 二叉堆根据排序不同,可以分为最大堆和最小堆

  • 最大堆:根节点的键值是所有堆节点键值中最大者,且每个父节点的值都比子节点的值大
  • 最小堆:根节点的键值是所有堆节点键值中最小者,且每个父节点的值都比子节点的值小

Untitled Diagram (1)

如何实现二叉堆

通过上面的讲述想必大家对二叉堆有了一定的理解,那么接下来就是如何实现。以最大堆为例,首先要初始化数组然后通过交换位置形成最大堆。

初始化二叉堆

从上面描述,我们可以知道二叉堆其实就是一个数组,那么初始化就非常简单了。

class Heap{
  constructor(arr){
    this.data = [...arr];
    this.size = this.data.length;
  }
}

父子节点交换位置

图一中 2 作为父节点小于子节点,很显然不符合最大堆性质。maxHeapify 函数可以把每个不符合最大堆性质的节点调换位置,从而满足最大堆性质的数组。

5

调整步骤:

1.调整分支节点 2 的位置(不满足最大堆性质)

2.获取父节点 2 的左右节点 ( 12 , 5 ) ,从 ( 2 , 15 , 5 ) 中进行比较

3.找出最大的节点与父节点进行交换,如果该节点本身为最大节点则停止操作

4.重复 step2 的操作,从 2 , 4 , 7 中找出最大值与 2 做交换(递归)

maxHeapify(i) {
  let max = i;

  if(i >= this.size){
    return;
  }
  // 当前序号的左节点
  const l = i * 2 + 1;
  // 当前需要的右节点
  const r = i * 2 + 2;

  // 求当前节点与其左右节点三者中的最大值
  if(l < this.size && this.data[l] > this.data[max]){
    max = l;
  }
  if(r < this.size && this.data[r] > this.data[max]){
    max = r;
  }

  // 最终max节点是其本身,则已经满足最大堆性质,停止操作
  if(max === i) {
    return;
  }

  // 父节点与最大值节点做交换
  const t = this.data[i];
  this.data[i] = this.data[max];
  this.data[max] = t;

  // 递归向下继续执行
  return this.maxHeapify(max);
}

形成最大堆

我们可以看到,初始化是由一个数组组成,以下图为例很显然并不会满足最大堆的性质,上述 maxHeapify 函数只是对某一个节点作出对调,无法对整个数组进行重构,所以我们要依次对数组进行递归重构。

6

1.找到所有分支节点 Math.floor( N / 2 )(不包括叶子节点)

2.将找到的子节点进行 maxHeapify 操作

rebuildHeap(){
  // 叶子节点
  const L = Math.floor(this.size / 2);
  for(let i = L - 1; i >= 0; i--){
    this.maxHeapify(i);
  }
}

生成一个升序的数组

B9AA42A8-8E58-4729-BF07-5164559E33BD

1.swap 函数交换首位位置

2.将最后一个从堆中拿出相当于 size - 1

3.执行 maxHeapify 函数进行根节点比较找出最大值进行交换

4.最终 data 会变成一个升序的数组

sort() {
  for(let i = this.size - 1; i > 0; i--){
    swap(this.data, 0, i);
    this.size--;
    this.maxHeapify(0);
  }
}

插入方法

Insert 函数作为插入节点函数,首先

1.往 data 结尾插入节点

2.因为节点追加,size + 1

3.因为一个父节点拥有 2 个子节点,我们可以根据这个性质通过 isHeap 函数获取第一个叶子节点,可以通过第一个叶子节点获取新插入的节点,然后进行 3 个值的对比,找出最大值,判断插入的节点。如果跟父节点相同则不进行重构(相等满足二叉堆性质),否则进行 rebuildHeap 重构堆

isHeap() {
  const L = Math.floor(this.size / 2);
  for (let i = L - 1; i >= 0; i--) {
    const l = this.data[left(i)] || Number.MIN_SAFE_INTEGER;
    const r = this.data[right(i)] || Number.MIN_SAFE_INTEGER;

    const max = Math.max(this.data[i], l, r);

    if (max !== this.data[i]) {
      return false;
    }
    return true;
  }
}
insert(key) {
  this.data[this.size] = key;
  this.size++
  if (this.isHeap()) {
    return;
  }
  this.rebuildHeap();
}

删除方法

delete 函数作为删除节点,首先

1.删除传入index的节点

2.因为节点删除,size - 1

3.重复上面插入节点的操作

delete(index) {
  if (index >= this.size) {
    return;
  }
  this.data.splice(index, 1);
  this.size--;
  if (this.isHeap()) {
    return;
  }
  this.rebuildHeap();
}

完整代码

/**
 * 最大堆
 */

function left(i) {
  return (i * 2) + 1;
}

function right(i) {
  return (i * 2) + 2;
}

function swap(A, i, j) {
  const t = A[i];
  A[i] = A[j];
  A[j] = t;
}

class Heap {
  constructor(arr) {
    this.data = [...arr];
    this.size = this.data.length;
    this.rebuildHeap = this.rebuildHeap.bind(this);
    this.isHeap = this.isHeap.bind(this);
    this.sort = this.sort.bind(this);
    this.insert = this.insert.bind(this);
    this.delete = this.delete.bind(this);
    this.maxHeapify = this.maxHeapify.bind(this);
  }

  /**
   * 重构堆,形成最大堆
   */
  rebuildHeap() {
    const L = Math.floor(this.size / 2);
    for (let i = L - 1; i >= 0; i--) {
      this.maxHeapify(i);
    }
  }

  isHeap() {
    const L = Math.floor(this.size / 2);
    for (let i = L - 1; i >= 0; i--) {
      const l = this.data[left(i)] || Number.MIN_SAFE_INTEGER;
      const r = this.data[right(i)] || Number.MIN_SAFE_INTEGER;

      const max = Math.max(this.data[i], l, r);

      if (max !== this.data[i]) {
        return false;
      }
      return true;
    }
  }

  sort() {
    for (let i = this.size - 1; i > 0; i--) {
      swap(this.data, 0, i);
      this.size--;
      this.maxHeapify(0);
    }
  }

  insert(key) {
    this.data[this.size++] = key;
    if (this.isHeap()) {
      return;
    }
    this.rebuildHeap();
  }

  delete(index) {
    if (index >= this.size) {
      return;
    }
    this.data.splice(index, 1);
    this.size--;
    if (this.isHeap()) {
      return;
    }
    this.rebuildHeap();
  }

  /**
   * 交换父子节点位置,符合最大堆特征
   * @param {*} i
   */
  maxHeapify(i) {
    let max = i;

    if (i >= this.size) {
      return;
    }

    // 求左右节点中较大的序号
    const l = left(i);
    const r = right(i);
    if (l < this.size && this.data[l] > this.data[max]) {
      max = l;
    }

    if (r < this.size && this.data[r] > this.data[max]) {
      max = r;
    }

    // 如果当前节点最大,已经是最大堆
    if (max === i) {
      return;
    }

    swap(this.data, i, max);

    // 递归向下继续执行
    return this.maxHeapify(max);
  }
}

module.exports = Heap;

示例

相信通过上面的讲述大家对最大堆的实现已经有了一定的理解,我们可以利用这个来进行排序。

const arr = [15, 12, 8, 2, 5, 2, 3, 4, 7];
const fun = new Heap(arr);
fun.rebuildHeap(); // 形成最大堆的结构
fun.sort();// 通过排序,生成一个升序的数组
console.log(fun.data) // [2, 2, 3, 4, 5, 7, 8, 12, 15]

总结

文章中主要讲述了二叉树、二叉堆的概念,然后通过代码实现二叉堆。我们可以通过二叉堆来做排序和优先级队列等。

推荐阅读

前端异常的捕获与处理

编写高质量可维护的代码:组件的抽象与粒度

招贤纳士

政采云前端团队(ZooTeam),一个年轻富有激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员构成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端应用、数据分析及可视化等方向进行技术探索和实战,推动并落地了一系列的内部技术产品,持续探索前端技术体系的新边界。

如果你想改变一直被事折腾,希望开始能折腾事;如果你想改变一直被告诫需要多些想法,却无从破局;如果你想改变你有能力去做成那个结果,却不需要你;如果你想改变你想做成的事需要一个团队去支撑,但没你带人的位置;如果你想改变既定的节奏,将会是“5 年工作时间 3 年工作经验”;如果你想改变本来悟性不错,但总是有那一层窗户纸的模糊… 如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望参与到随着业务腾飞的过程,亲手推动一个有着深入的业务理解、完善的技术体系、技术创造价值、影响力外溢的前端团队的成长历程,我觉得我们该聊聊。任何时间,等着你写点什么,发给 ZooTeam@cai-inc.com

查看原文

CobraJ 回答了问题 · 3月2日

有没有像语雀编辑器一样的表格插件?

关注 2 回答 1

CobraJ 回答了问题 · 2月22日

socket.io.js和require.js冲突

image.png
看了看源码貌似支持define,就支持requirejs啊,你用法贴出来看看吧

关注 1 回答 1

CobraJ 收藏了文章 · 2月22日

从 left-pad 事件我们可以学到什么

最近 NPM 圈发生了“一个 17 行的模块引发的血案”。left-pad 工具模块被作者从 NPM 上撤下,所有直接或者间接依赖这个模块的 NPM 包就忧伤的挂掉了,包括 babel 这样的热门项目。

而其中的原因大概是这样:作者 Azer 写了一个叫 kik 的工具和某个公司同名了,这天公司的律师要求其删掉这个模块,把 kik 这个名字“让”给他们,作者不答应,律师就直接找 NPM 了,而 NPM 未经作者同意就把包的权限转移给了这家公司。于是,Azer 一怒冲冠,将他所有的 NPM 包全部删掉了。

我们不打算讨论这件事中的价值观、自由之精神、法律细节等等,我只想站在一个程序员的角度来凑个热闹,聊聊看法。

细分模块是必要的

有人会觉得就这几行代码有必要单独抽取成一个模块吗?我觉得有必要,原因如下:

  • 你应该不想把这个工具函数在各个项目里面重复的去复制粘贴,毕竟 DRY(Don't Repeat Yourself) 嘛。

  • 人们管理软件复杂度的通常方法都是拆分,写好一个模块,做好测试,然后直接使用这个模块。“small module” 的想法本来就是很好的。

  • 另外一种想法是我所有的工具函数都统一放在一个模块,这很容易造成工具模块越来越大,什么都往里放。而别人使用这个庞大的工具模块时,很可能其中很多东西根本就用不上。

  • 还有人说这个功能根本不用这么多代码,但是到处写这一行代码也挺烦不是?况且你也不知道一个几行代码能解决的问题有可能随着需求的变化需要更复杂的实现(比如:更多的定制性、性能要求)。

所以,NPM 一直提倡和推动的 small module 的理念并没有什么错,我们不应该因为这个事件就把各种“小模块”的依赖从自己项目中去掉,甚至自己也不写“小模块”了。

不要太依赖他人

如果我们想构建稳定的应用,非常重要的一点就是“不要将自己的全部身家都拴在别人的裤腰带”上,你永远不知道那个别人什么时候会“扑街”。其实在代码实现中,我们一直都被教导要小心强依赖,依赖过强会导致我们不够灵动。“牵一发而动全身”是很可怕的一件事。

具体到 Node 模块的依赖这件事上,太依赖他人会有些什么“可怕”的事情?

  1. 模块全部要从 NPM 的 registry 拉取然后安装,每天的持续集成越来越慢、越来越慢。

  2. 像 left-pad 这个模块一样,你依赖的模块被作者怒删了,应用挂掉。

  3. 你在 package.json 里面指定依赖时使用了 ~a.b.c 这种表示法(意思是小版本我都要),这表示每次 npm install 时其实获取到的模块依赖很可能是和你测试后发布的版本不一致的(模块发布了新的小版本),心里慌慌的。

  4. 你依赖模块的作者是个傻逼,不小心将不兼容的改动当作小版本发布了一个新版。npm install 或者 npm update 以后你依赖了这个新版,应用挂掉了。

注:对 3、4 有疑问的可以查看:https://segmentfault.com/a/1190000004221514

是不是有点“细思极恐”?那这里抛出一个解决方案。如果有更好的方案,欢迎讨论。

破解强依赖

先来列出我们需要些什么:

  • 在发布前“冻结”依赖模块的版本号。这让我们对安装的依赖有信心,依赖模块的版本都是我们验证、测试过的。

  • 在发布前“打包”依赖模块到自己项目。这让我们可以坦然面对我们依赖的某个模块“没有了”这样的囧境。

冻结依赖模块

冻结依赖模块的版本号最简单的办法就是直接在 package.json 里面写死版本号,但是这解决不了深度依赖的问题。我们来看个例子。

假设有下面这样的依赖:

A@0.1.0
└─┬ B@0.0.1
  └── C@0.0.1

A 模块依赖了 B 模块,B 模块又依赖了 C 模块。我们可以将 B 模块的依赖写死成 0.0.1 版本,但是如果 B 模块对 C 模块的依赖写的是 C: ~0.0.1,会怎样?

这时候 C 模块更新到了 0.0.2 版本,虽然我们安装的 B 模块是 B@0.0.1,但是安装的 C 模块却是 C@0.0.2。如果不巧这个 C@0.0.2 刚好有 bug,那我们的模块很有可能就不能正常工作了。

实际上,NPM 提供了一个叫做 npm shrinkwrap 的命令来我们解决这个问题:

NAME
  npm-shrinkwrap -- Lock down dependency versions

SYNOPSIS
  npm shrinkwrap

DESCRIPTION
  This  command  locks down the versions of a package's dependencies so that you can control exactly which versions of each  dependency  will be used when your package is installed.

这条命令会根据目前我们 node_modules 目录下的模块来生成一份“冻结”住的模块依赖(npm-shrinkwrap.json)。

还是上面的例子,我们在模块 A 的根目录执行 npm shrinkwrap 后,生成的 npm-shrinkwrap.json 文件内容大概是下面这样:

{
    "name": "A",
    "dependencies": {
        "B": {
            "version": "0.0.1",
            "resolved": "http://registry.npmjs.com/B-0.0.1.tgz",
            "dependencies": {
                "C": {  
                    "version": "0.0.1",
                    "resolved": "http://registry.npmjs.com/C-0.0.1.tgz"
                }
            }
        }
    }
}

然后,当我们执行 npm install 时,依赖查找的“来源”不再是 package.json,而是我们生成的 npm-shrinkwrap.json,再也不会突然装上什么 C@0.0.2 了,依赖里面的模块版本都是我们验证、测试后的版本,让人安心。

注:npm shrinkwrap 默认只会生成 dependencies 的依赖,不会生成 devDependencies 的依赖,如果你真的需要,可以加 --dev 参数。

打包依赖模块

我们解决了依赖模块版本号的问题,但是每次安装时其实还是会去 NPM 的 registry 获取模块的 tgz 包然后进行安装。我们需要将这些依赖都打包进我们的项目。这可能会带来一些问题(比如:项目体积的增大),但是好处也是显而易见的。

上面生成的 npm-shrinkwrap.json 里面有个 resolved 字段,表示模块所在的位置,实际上这个字段完全可以写一个文件路径。所以,我们可以递归的遍历 npm-shrinkwrap.json 文件,将所有的 tgz 包先下载到我们项目的某个目录,然后改写 resolved 字段为对应的文件路径。这样的功能有开发者已经实现了,我们可以直接享用:https://github.com/JamieMason/shrinkpack

还是上面的例子:

A@0.1.0
└─┬ B@0.0.1
  └── C@0.0.1

执行 shrinkpack 后,会生成下面的打包目录:

node_shrinkpack
 - B-0.0.1.tgz
 - C-0.0.1.tgz

node-shrinkwrap.json 文件:

{
    "name": "A",
    "dependencies": {
        "B": {
            "version": "0.0.1",
            "resolved": "./node_shrinkpack/B-0.0.1.tgz",
            "dependencies": {
                "C": {  
                    "version": "0.0.1",
                    "resolved": "./node_shrinkpack/C-0.0.1.tgz"
                }
            }
        }
    }
}

于是,我们以后再进行 npm install --loglevel=http 时会发现依赖模块的获取根本没有网络请求了(因为依赖都在我们自己的仓库里了嘛)。

可能有人会说,为啥不直接把 node_modules 目录提交进仓库算了?原因主要是这样:

  • 有些模块需要编译,编译是和环境有关的,你当前的环境编译可用,其他环境直接使用该模块不一定能用。

  • node_modules 目录里面啥东西都有,太凌乱,很容易把提交给搅乱。diff 时突然 diff 出 node_modules 下的源代码、README,你应该不想这样吧?

只存储模块的 tgz 包,安装编译的过程交给 NPM 命令更明智。

新方式

于是,现在我们使用 NPM 模块的正确姿势应该是这样了:

  1. 本地安装、更新需要的模块,测试、验证

  2. 执行 npm shrinkwrap 将依赖模块的版本冻结

  3. 执行 shrinkpack . 将依赖模块打包进仓库

  4. 提交代码(注意要将 npm-shrinkwrap.jsonnode_shrinkpack 一起提交哦)

  5. 发布模块或者部署应用

如果你觉得这样很繁琐,可以定义一个 NPM 命令:

"scripts": {
  "pack": "npm shrinkwrap & shrinkpack ."
}

总结

  • 拆分模块是必要的,我们应该坚持模块“小而美”

  • 不要太依赖他人,一定要有依赖方挂掉的应急方案

  • 推荐使用 npm shrinkwrap(冻结依赖模块的版本) 加 shrinkpack(打包依赖模块到自己项目) 来解决依赖模块的不确定性

参考资料

查看原文

CobraJ 收藏了文章 · 1月27日

理解 JavaScript 的 async/await

2020-06-04 更新

JavaScript 中的 async/await 是 AsyncFunction 特性 中的关键字。目前为止,除了 IE 之外,常用浏览器和 Node (v7.6+) 都已经支持该特性。具体支持情况可以在 这里 查看。


我第一次看到 async/await 这组关键字并不是在 JavaScript 语言里,而是在 C# 5.0 的语法中。C# 的 async/await 需要在 .NET Framework 4.5 以上的版本中使用,因此我还很悲伤了一阵——为了要兼容 XP 系统,我们开发的软件不能使用高于 4.0 版本的 .NET Framework。

我之前在《闲谈异步调用“扁平”化》 中就谈到了这个问题。无论是在 C# 还是 JavaScript 中,async/await 都是非常棒的特性,它们也都是非常甜的语法糖。C# 的 async/await 实现离不开 Task 或 Task\<Result\> 类,而 JavaScript 的 async/await 实现,也离不开 Promise

现在抛开 C# 和 .NET Framework,专心研究下 JavaScript 的 async/await。

1. async 和 await 在干什么

任意一个名称都是有意义的,先从字面意思来理解。async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

另外还有一个很有意思的语法规定,await 只能出现在 async 函数中。然后细心的朋友会产生一个疑问,如果 await 只能出现在 async 函数中,那这个 async 函数应该怎么调用?

如果需要通过 await 来调用一个 async 函数,那这个调用的外面必须得再包一个 async 函数,然后……进入死循环,永无出头之日……

如果 async 函数不需要 await 来调用,那 async 到底起个啥作用?

1.1. async 起什么作用

这个问题的关键在于,async 函数是怎么处理它的返回值的!

我们当然希望它能直接通过 return 语句返回我们想要的值,但是如果真是这样,似乎就没 await 什么事了。所以,写段代码来试试,看它到底会返回什么:

async function testAsync() {
    return "hello async";
}

const result = testAsync();
console.log(result);

看到输出就恍然大悟了——输出的是一个 Promise 对象。

c:\var\test> node --harmony_async_await .
Promise { 'hello async' }

所以,async 函数返回的是一个 Promise 对象。从文档中也可以得到这个信息。async 函数(包含函数语句、函数表达式、Lambda表达式)会返回一个 Promise 对象,如果在函数中 return 一个直接量,async 会把这个直接量通过 Promise.resolve() 封装成 Promise 对象。

补充知识点 [2020-06-04]

Promise.resolve(x) 可以看作是 new Promise(resolve => resolve(x)) 的简写,可以用于快速封装字面量对象或其他对象,将其封装成 Promise 实例。

async 函数返回的是一个 Promise 对象,所以在最外层不能用 await 获取其返回值的情况下,我们当然应该用原来的方式:then() 链来处理这个 Promise 对象,就像这样

testAsync().then(v => {
    console.log(v);    // 输出 hello async
});

现在回过头来想下,如果 async 函数没有返回值,又该如何?很容易想到,它会返回 Promise.resolve(undefined)

联想一下 Promise 的特点——无等待,所以在没有 await 的情况下执行 async 函数,它会立即执行,返回一个 Promise 对象,并且,绝不会阻塞后面的语句。这和普通返回 Promise 对象的函数并无二致。

那么下一个关键点就在于 await 关键字了。

1.2. await 到底在等啥

一般来说,都认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的计算结果是 Promise 对象或者其它值(换句话说,就是没有特殊限定)。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数,但要清楚,它等的实际是一个返回值。注意到 await 不仅仅用于等 Promise 对象,它可以等任意表达式的结果,所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行

function getSomething() {
    return "something";
}

async function testAsync() {
    return Promise.resolve("hello async");
}

async function test() {
    const v1 = await getSomething();
    const v2 = await testAsync();
    console.log(v1, v2);
}

test();

1.3. await 等到了要等的,然后呢

await 等到了它要等的东西,一个 Promise 对象,或者其它值,然后呢?我不得不先说,await 是个运算符,用于组成表达式,await 表达式的运算结果取决于它等的东西。

如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的东西。

如果它等到的是一个 Promise 对象,await 就忙起来了,它会阻塞后面的代码,等着 Promise 对象 resolve,然后得到 resolve 的值,作为 await 表达式的运算结果。

看到上面的阻塞一词,心慌了吧……放心,这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。

2. async/await 帮我们干了啥

2.1. 作个简单的比较

上面已经说明了 async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。

现在举例,用 setTimeout 模拟耗时的异步操作,先来看看不用 async/await 会怎么写

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

takeLongTime().then(v => {
    console.log("got", v);
});

如果改用 async/await 呢,会是这样

function takeLongTime() {
    return new Promise(resolve => {
        setTimeout(() => resolve("long_time_value"), 1000);
    });
}

async function test() {
    const v = await takeLongTime();
    console.log(v);
}

test();

眼尖的同学已经发现 takeLongTime() 没有申明为 async。实际上,takeLongTime() 本身就是返回的 Promise 对象,加不加 async 结果都一样,如果没明白,请回过头再去看看上面的“async 起什么作用”。

又一个疑问产生了,这两段代码,两种方式对异步调用的处理(实际就是对 Promise 对象的处理)差别并不明显,甚至使用 async/await 还需要多写一些代码,那它的优势到底在哪?

2.2. async/await 的优势在于处理 then 链

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

假设一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果。我们仍然用 setTimeout 来模拟异步操作:

/**
 * 传入参数 n,表示这个函数执行的时间(毫秒)
 * 执行的结果是 n + 200,这个值将用于下一步骤
 */
function takeLongTime(n) {
    return new Promise(resolve => {
        setTimeout(() => resolve(n + 200), n);
    });
}

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(n) {
    console.log(`step2 with ${n}`);
    return takeLongTime(n);
}

function step3(n) {
    console.log(`step3 with ${n}`);
    return takeLongTime(n);
}

现在用 Promise 方式来实现这三个步骤的处理

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => step2(time2))
        .then(time3 => step3(time3))
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 500
// step3 with 700
// result is 900
// doIt: 1507.251ms

输出结果 resultstep3() 的参数 700 + 200 = 900doIt() 顺序执行了三个步骤,一共用了 300 + 500 + 700 = 1500 毫秒,和 console.time()/console.timeEnd() 计算的结果一致。

如果用 async/await 来实现呢,会是这样

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time2);
    const result = await step3(time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

结果和之前的 Promise 实现是一样的,但是这个代码看起来是不是清晰得多,几乎跟同步代码一样

2.3. 还有更酷的

现在把业务要求改一下,仍然是三个步骤,但每一个步骤都需要之前每个步骤的结果。

function step1(n) {
    console.log(`step1 with ${n}`);
    return takeLongTime(n);
}

function step2(m, n) {
    console.log(`step2 with ${m} and ${n}`);
    return takeLongTime(m + n);
}

function step3(k, m, n) {
    console.log(`step3 with ${k}, ${m} and ${n}`);
    return takeLongTime(k + m + n);
}

这回先用 async/await 来写:

async function doIt() {
    console.time("doIt");
    const time1 = 300;
    const time2 = await step1(time1);
    const time3 = await step2(time1, time2);
    const result = await step3(time1, time2, time3);
    console.log(`result is ${result}`);
    console.timeEnd("doIt");
}

doIt();

// c:\var\test>node --harmony_async_await .
// step1 with 300
// step2 with 800 = 300 + 500
// step3 with 1800 = 300 + 500 + 1000
// result is 2000
// doIt: 2907.387ms

除了觉得执行时间变长了之外,似乎和之前的示例没啥区别啊!别急,认真想想如果把它写成 Promise 方式实现会是什么样子?

function doIt() {
    console.time("doIt");
    const time1 = 300;
    step1(time1)
        .then(time2 => {
            return step2(time1, time2)
                .then(time3 => [time1, time2, time3]);
        })
        .then(times => {
            const [time1, time2, time3] = times;
            return step3(time1, time2, time3);
        })
        .then(result => {
            console.log(`result is ${result}`);
            console.timeEnd("doIt");
        });
}

doIt();

有没有感觉有点复杂的样子?那一堆参数处理,就是 Promise 方案的死穴—— 参数传递太麻烦了,看着就晕!

3. 洗洗睡吧

就目前来说,已经理解 async/await 了吧?但其实还有一些事情没提及——Promise 有可能 reject 啊,怎么处理呢?如果需要并行处理3个步骤,再等待所有结果,又该怎么处理呢?

阮一峰老师已经说过了,我就懒得说了。

4. 推荐相关文章

5. 来跟边城(作者)学 更新@2020-11-14

TypeScript从入门到实践 【2020 版】

TypeScript从入门到实践 【2020 版】

6. 关于转载 补充@2020-03-05

常有读者问是否可以转载。

笔者表示欢迎各位转载,但转载时一定注明作者和出处,谢谢!


公众号-边城客栈
请关注公众号 边城客栈

看完了先别走,点个赞啊 ⇓,赞赏 ⇘ 也行!

查看原文

认证与成就

  • 获得 29 次点赞
  • 获得 18 枚徽章 获得 1 枚金徽章, 获得 4 枚银徽章, 获得 13 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-08-18
个人主页被 1.2k 人浏览