整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

14 个实用又简洁的单行 JS 代码,好用到飞起!

14 个实用又简洁的单行 JS 代码,好用到飞起!

编程中,解决同一个问题通常有多种方法。这些解决方案在不同方面可能有所不同,例如长度、性能、使用的算法、可读性等。

在本文中,我们将研究几种快速简洁的单行解决方案,以解决 JavaScript 中经常出现的各种问题。

什么是单行代码?

在我们开始之前,让我们确保我们了解是什么单行代码。

单行代码是问题的代码解决方案,使用特定编程语言中的单个语句实现,无需任何第三方实用程序。

该定义包含许多其他定义中没有的重要区别特征:

1). “……单句……”

并非每一段只占用一行的代码都是单行代码。例如,看看这个将两个平方和相加并返回结果的方法。

const sum=(a, b)=> { const s1=a * a; const s2=b * b; return s1 + s2; }

你会称之为单行代码吗?在大多数情况下,这只会作为格式错误的代码通过。Prettier 之类的工具可以轻松地将这三个语句自动拆分为多行。

获得两个平方和的真正单行方法是这样的:

const sum=(a, b)=> a * a + b * b;

一个简短、简洁的陈述可以同样清晰地完成同样的工作。

另一方面,此方法跨越多行代码以提高可读性,但它仍然可以作为一行代码通过:

const capitalizeWithoutSpaces=(str)=>
  str
    .split('')
    .filter((char)=> char.trim())
    .map((char)=> char.toUpperCase())
    .join('');

因此,尽管名称如此,“单行”并不一定意味着是一行代码。

2). “……特定的编程语言……”

这与每个高级语言程序在执行前都必须翻译成低级语言这一事实有关。一个非常简单的程序最终可能会占用数十或数百行汇编代码和机器代码。

例如,这里是另一个也添加两个平方和的单行代码,这次是在 C++ 中:

int sum(int a, int b) {
    return a * a + b * b;
}

让我们看看编译成汇编语言后的样子:

sum(int, int):
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        mov     eax, DWORD PTR [rbp-4]
        imul    eax, eax
        mov     edx, eax
        mov     eax, DWORD PTR [rbp-8]
        imul    eax, eax
        add     eax, edx
        pop     rbp
        ret

这个汇编程序显然不止一行或一行代码。想象一下等效的机器语言程序会有多少。所以这个函数可以说是仅在 C++ 上下文中的单行函数。

3). “……没有任何第三方实用程序”

对于单行代码,它不应该引用编程语言本身不可用的任何方法或函数,记住我们之前看过的单行代码:

const capitalizeWithoutSpaces=(str)=>
  str
    .split('')
    .filter((char)=> char.trim())
    .map((char)=> char.toUpperCase())
    .join('');

这里使用的所有方法都是内置的 JavaScript 方法。它不包括来自 NPM 或其他地方的第三方代码。

但是,如果我们决定实现自己的 filter() 方法来替换 Array filter(),则该方法将不再符合单行方法的条件。

// Not a one-liner
const capitalizeWithoutSpaces=(str)=>
  filter(str.split(''), (char)=> char.trim())
    .map((char)=> char.toUpperCase())
    .join('');
function filter(arr, callback) {
  // Look at all these lines
  const result=[];
  for (const item of arr) {
    if (callback(item)) {
      result.push(item);
    }
  }
  return result;
}

抛开定义,现在让我们看一些聪明的 JavaScript 单行代码以及它们解决方案。

1. 获取数组的最小元素

要获得数组中的最小项,我们可以采用这种使用 for 循环和 if 语句的命令式方法。

const getSmallest=(arr)=> {
  let smallest=Number.POSITIVE_INFINITY;
  for (const num of arr) {
    if (num < smallest) {
      smallest=num;
    }
  }
  return smallest;
};
const arr=[13, 7, 11, 3, 9, 15, 17];
console.log(getSmallest(arr)); // 3

这没关系,但有一个简洁且声明性的单行替代方案同样有效:

const getSmallest=(arr)=>
  arr.reduce((smallest, num)=> Math.min(smallest, num));
const arr=[13, 7, 11, 3, 9, 15, 17];
console.log(getSmallest(arr)); // 3

2. 获取数组的最大元素

这是获取数组中最大元素的可接受方法。

const getLargest=(arr)=> {
  let largest=Number.NEGATIVE_INFINITY;
  for (const num of arr) {
    if (num > largest) {
      largest=num;
    }
  }
  return largest;
};
const arr=[13, 7, 11, 3, 9, 15, 17];
console.log(getLargest(arr)); // 17

但就像我们看到的获取最小数组元素一样,有一种更短、更简洁的方法。

const getLargest=(arr)=>
  arr.reduce((largest, num)=> Math.max(largest, num));
const arr=[13, 7, 11, 3, 9, 15, 17];
console.log(getLargest(arr)); // 17

您可以看到,此函数与单行 getSmallest() 函数之间的唯一区别是 Math.min() 已替换为 Math.max()。

3. 打乱数组

数组/列表洗牌的一个常见用途是在纸牌游戏中,其中牌组中的牌必须随机排序。

Fisher-Yates 洗牌是一种著名的洗牌算法。查看它在 JavaScript 中的可能实现:

const shuffleArray=(arr)=> {
  for (let i=arr.length - 1; i > 0; i--) {
    const j=Math.floor(Math.random() * (i + 1));
    let temp=arr[i];
    arr[i]=arr[j];
    arr[j]=temp;
  }
  return arr;
};
const arr=[1, 2, 3, 4, 5];
shuffleArray(arr);
// [ 2, 3, 5, 1, 4 ] (varies)
console.log(arr);

用一些函数式编程魔法重构它,我们有:

const shuffleArray=(arr)=>
  [...Array(arr.length)]
    .map((_, i)=> Math.floor(Math.random() * (i + 1)))
    .reduce(
      (shuffled, r, i)=>
        shuffled.map((num, j)=>
          j===i ? shuffled[r] : j===r ? shuffled[i] : num
        ),
      arr
    );
// [ 2, 4, 1, 3, 5 ] (varies)
console.log(shuffleArray([1, 2, 3, 4, 5]));

这以 O(n2) 时间复杂度(二次)运行,并且可能会导致大型数组出现性能问题,但它是一种优雅的解决方案。此外,与第一种方法不同,它不会改变原始数组。

另一种函数式方法利用 Array sort() 方法的实现方式来随机排列数组。

const shuffleArray=(arr)=> arr.sort(()=> Math.random() - 0.5);
const arr=[1, 2, 3, 4, 5];
// [ 5, 2, 4, 1, 3 ] (varies)
console.log(shuffleArray(arr));

由于它使用了 sort(),因此,它的运行时间复杂度为 O(n log n),并且比前面的方法具有更好的性能。

4. 按对象属性对数组进行分组

有时我们需要使用它们都具有的特定属性对一组对象进行分组,例如,按国家/地区对用户进行分组,按出版年份对书籍进行分组,按颜色对汽车进行分组等。

在下面的示例中,我们根据姓名的长度将人物对象分组到一个数组中。

const groupBy=(arr, groupFn)=> {
  const grouped={};
  for (const obj of arr) {
    const groupName=groupFn(obj);
    if (!grouped[groupName]) {
      grouped[groupName]=[];
    }
    grouped[groupName].push(obj);
  }
  return grouped;
};
const people=[
  { name: 'Matt' },
  { name: 'Sam' },
  { name: 'John' },
  { name: 'Mac' },
];
const groupedByNameLength=groupBy(people, (person)=> person.name.length);
/**
{
  '3': [ { name: 'Sam' }, { name: 'Mac' } ],
  '4': [ { name: 'Matt' }, { name: 'John' } ]
}
 */
console.log(groupedByNameLength);

这是单行代码的解决方案:

const groupBy=(arr, groupFn)=>
  arr.reduce(
    (grouped, obj)=> ({
      ...grouped,
      [groupFn(obj)]: [...(grouped[groupFn(obj)] || []), obj],
    }),
    {}
  );
const people=[
  { name: 'Matt' },
  { name: 'Sam' },
  { name: 'John' },
  { name: 'Mac' },
];
const groupedByNameLength=groupBy(people, (person)=> person.name.length);
/**
{
  '3': [ { name: 'Sam' }, { name: 'Mac' } ],
  '4': [ { name: 'Matt' }, { name: 'John' } ]
}
 */
console.log(groupedByNameLength);

5.反转字符串

我们可以在 JavaScript 中使用反向 for 循环来反转字符串,如下所示:

const reverseString=(str)=> {
  let reversed='';
  for (let i=str.length - 1; i >=0; i--) {
    const ch=str[i];
    reversed +=ch;
  }
  return reversed;
};
const reverse=reverseString('javascript');
console.log(reverse); // tpircsavaj

但是再一次,我们可以利用强大的内置数组方法,如 reverse() 和 join() 来创建一个做同样事情的单行代码。

const reverseString=(str)=> str.split('').reverse().join('');
const reverse=reverseString('javascript');
console.log(reverse); // tpircsavaj

6. 生成随机的十六进制颜色

十六进制颜色代码是指定 RGB 颜色的一种方式。它们具有#RRGGBB 格式,其中 RR 代表红色,GG 代表绿色,BB 代表蓝色。每种颜色的值范围从 0 到 255,并以十六进制格式表示 - 0 到 FF。

这个单行生成一个随机的十六进制颜色并返回结果。

const randomHexColor=()=>
  `#${Math.random().toString(16).slice(2, 8).padEnd(6, '0')}`;
console.log(randomHexColor()); // #7a10ba (varies)
console.log(randomHexColor()); // #65abdc (varies)

7. 获取数组的平均值

这是众多问题中的另一个问题,其中涉及循环的解决方案可以使用一种或多种 Array 方法来缩短。

因此,虽然我们可以像这样获得数组中数字的平均值:

const getAverage=(arr)=> {
  let sum=0;
  for (const num of arr) {
    sum +=num;
  }
  return sum / arr.length;
};
const arr=[5, 13, 9, 11, 10, 15, 7];
const average=getAverage(arr);
console.log(average); // 10

Array reduce() 方法让我们创建了这个紧凑的单行替代方案:

const getAverage=(arr)=> arr.reduce((sum, num)=> sum + num, 0) / arr.length;
const arr=[5, 13, 9, 11, 10, 15, 7];
const average=getAverage(arr);
console.log(average); // 10

8. 检查两个数组是否包含相同的值

这是一个确保两个数组包含相同元素(以任何顺序)并且这些元素在两个数组中出现相同次数的问题。

使用 for 循环,我们可以实现以下解决方案:

const areEqual=(arr1, arr2)=> {
  if (arr1.length===arr2.length) {
    for (const num of arr1) {
      if (!arr2.includes(num)) {
        return false;
      }
    }
    return true;
  }
  return false;
};
const arr1=[1, 2, 3, 4];
const arr2=[3, 4, 1, 2];
const arr3=[1, 2, 3];
console.log(areEqual(arr1, arr2)); // true
console.log(areEqual(arr1, arr3)); // false

使用 Array sort() 和 join() 方法,我们可以创建这个单行替代方案:

const areEqual=(arr1, arr2)=>
  arr1.sort().join(',')===arr2.sort().join(',');
const arr1=[1, 2, 3, 4];
const arr2=[3, 4, 1, 2];
const arr3=[1, 2, 3];
console.log(areEqual(arr1, arr2)); // true
console.log(areEqual(arr1, arr3)); // false

9. 从数组中删除重复项

我们可以像这样从数组中删除重复项:

const removeDuplicates=(arr)=> {
  const result=[];
  for (const num of arr) {
    if (!result.includes(num)) {
      result.push(num);
    }
  }
  return result;
};
const arr=[1, 2, 3, 4, 5, 3, 1, 2, 5];
const distinct=removeDuplicates(arr);
console.log(distinct); // [1, 2, 3, 4, 5]

但是我们可以利用 Set() 构造函数在短短一行中删除重复项:

const removeDuplicates=(arr)=> [...new Set(arr)];
const arr=[1, 2, 3, 4, 5, 3, 1, 2, 5];
const distinct=removeDuplicates(arr);
console.log(distinct); // [1, 2, 3, 4, 5]

10. 将Map转换为 JSON

这个简短的函数让我们可以快速将 Map 对象转换为 JSON 字符串而不会丢失任何信息:

const mapToJson=(map)=> JSON.stringify(Object.fromEntries(map));
const map=new Map([
  ['user1', 'John'],
  ['user2', 'Kate'],
  ['user3', 'Peter'],
]);
const json=mapToJson(map);
// {"user1":"John","user2":"Kate","user3":"Peter"}
console.log(json);

11. 将 JSON 转换为Map

另一个一行可以反转上面的转换。以下函数会将 JSON 字符串转换为 Map 对象。

const jsonToMap=(json)=> new Map(Object.entries(JSON.parse(json)));
const json='{"user1":"John","user2":"Kate","user3":"Peter"}';
const map=jsonToMap(json);
// Kate
console.log(map.get('user2'));
// Map(3) { 'user1'=> 'John', 'user2'=> 'Kate', 'user3'=> 'Peter' }
console.log(map);

12. 将蛇形字符串转换为驼峰大小写

在蛇形字符串中,每个单词由下划线 (_) 分隔并以小写字母开头,例如:variable_name、bread_and_eggs 等。

但是,对于驼峰式字符串,第一个单词以小写字母开头,后面的单词均以大写字母开头。单词之间没有空格或标点符号。驼峰式字符串的示例有:variableName、breadAndEggs 等。

使用这个简洁的函数,我们可以将任何蛇形大小写的字符串转换为驼峰大小写。

const snakeToCamelCase=(s)=>
  s.toLowerCase().replace(/(_\w)/g, (w)=> w.toUpperCase().substr(1));
const str1='learn_javascript';
const str2='coding_beauty';
console.log(snakeToCamelCase(str1)); // learnJavaScript
console.log(snakeToCamelCase(str2)); // codingBeauty

13.生成随机UUID

“UUID”是大学唯一标识符的首字母缩写词。UUID 是一个 128 位的值,可唯一标识 Internet 上的对象或实体。

这个单行生成一个随机 UUID:

const generateRandomUUID=(a)=>
  a
    ? (a ^ ((Math.random() * 16) >> (a / 4))).toString(16)
    : ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(
        /[018]/g,
        generateRandomUUID
      );
console.log(generateRandomUUID()); // f138f635-acbd-4f78-9be5-ca3198c4cf34
console.log(generateRandomUUID()); // 8935bb0d-6503-441f-bb25-7bc685b5b5bc

14.条件流控制

我们可以使用嵌套的三元运算符将 if...else 或 switch 语句转换为单行语句。考虑一个返回特定范围内数字的英文单词形式的函数。

使用 if...else 语句,这样的函数可以这样实现:

const getNumWord=(num)=> {
  if (num===1) {
    return 'one';
  } else if (num===2) {
    return 'two';
  } else if (num===3) {
    return 'three';
  } else if (num===4) {
    return 'four';
  } else return 'unknown';
};
console.log(getNumWord(1)); // one
console.log(getNumWord(3)); // three
console.log(getNumWord(7)); // unknown

使用 switch...case 语句:

const getNumWord=(num)=> {
  switch (num) {
    case 1:
      return 'one';
    case 2:
      return 'two';
    case 3:
      return 'three';
    case 4:
      return 'four';
    default:
      return 'unknown';
  }
};
console.log(getNumWord(1)); // one
console.log(getNumWord(3)); // three
console.log(getNumWord(7)); // unknown

现在使用嵌套的三元组来创建单行代码:

const getNumWord=(num)=>
  num===1
    ? 'one'
    : num===2
    ? 'two'
    : num===3
    ? 'three'
    : num===4
    ? 'four'
    : 'unknown';
console.log(getNumWord(1)); // one
console.log(getNumWord(3)); // three
console.log(getNumWord(7)); // unknown

我们已经了解了针对常见JavaScript编程问题的简明解决方案。我们看到许多实例,其中包含多个语句的命令式解决方案被转换为使用各种内置方法和语言结构的声明式单行代码。

这些紧凑的解决方案有时性能和可读性较低,但使用它们可以证明您的编程能力和对语言的掌握程度。使用任何一种方法,我们都是需要根据具体的情况来使用。

不知道今年大家有没有感受到来自互联网的“寒气”,至少我是感受到了,面试的时候手写代码时很常见很常见的事情了。有时候没遇到过还真一时半会写不出来,企业招聘的要求也是越来越高,尤其是一些大厂会对 JS 的功底有着更加苛刻的要求,所以学会手写常见的 JS 模块好像已经快变为一个基本技能了,也慢慢变为我们手写 webpack 手写 mini-vue 的一个 coding 基础了。

当然我们也不完全是为了去准备面试而去学习这些常见模块。死磕这些难啃的骨头之后,你会从中学到很多优秀的思想,对你的职业生涯也是很有帮助的。而且阅读代码本身就是一个很好的习惯,读懂并且理解写代码人的思维逻辑更加重要。

本文中涉及到的手写模块,大多数都是从网上以及自己的面试经验借鉴而来的。希望能对你有个帮助。

基础手写

全排列(力扣原题)

要求以数组的形式返回字符串参数的所有排列组合。

注意:

  1. 字符串参数中的字符无重复且仅包含小写字母
  2. 返回的排列组合数组不区分顺序
const _permute=string=> {
  const result=[]
  const map=new Map()
  const dfs=(path)=> {
    if (path.length===string.length) {
      result.push(path)
      return
    }
    for (let i=0; i < string.length; i++) {
      if (map.get(string[i])) continue
      map.set(string[i], true)
      path +=string[i]
      dfs(path)
      path=path.substring(0, path.length - 1)
      map.set(string[i], false)
    }
  }
  dfs('')
  return result
}
console.log(_permute('abc')) // [ 'abc', 'acb', 'bac', 'bca', 'cab', 'cba' ]

instanceof

  • 如果 target 为基本数据类型直接返回 false
  • 判断 Fn.prototype 是否在 target 的隐式原型链上
const _instanceof=(target, Fn)=> {
  if ((typeof target !=='object' && typeof target !=='function') || target===null)
  return false
  
  let proto=target.__proto__
  while (true) {
    if (proto===null) return false
    if (proto===Fn.prototype) return true
    proto=proto.__proto__
  }
}
function A() {}
const a=new A()
console.log(_instanceof(a, A)) // true
console.log(_instanceof(1, A)) // false

Array.prototype.map

  • map 中的 exc 接受三个参数,分别是: 元素值、元素下标和原数组
  • map 返回的是一个新的数组,地址不一样
// 这里不能直接使用箭头函数,否则无法访问到 this
Array.prototype._map=function (exc) {
  const result=[]
  this.forEach((item, index, arr)=> {
    result[index]=exc(item, index, arr)
  })
  return result
}
const a=new Array(2).fill(2)
console.log(a.map((item, index, arr)=> item * index + 1)) // [1,3]
console.log(a._map((item, index, arr)=> item * index + 1))// [1,3]

Array.prototype.filter

  • filter 中的 exc 接受三个参数,与map一致,主要实现的是数组的过滤功能,会根据 exc 函数的返回值来判断是否“留下”该值。
  • filter 返回的是一个新的数组,地址不一致。
Array.prototype._filter=function (exc) {
  const result=[]
  this.forEach((item, index, arr)=> {
    if (exc(item, index, arr)) {
      result.push(item)
    }
  })
  return result
}
const b=[1, 3, 4, 5, 6, 2, 5, 1, 8, 20]

console.log(b._filter(item=> item % 2===0)) // [ 4, 6, 2, 8, 20 ]

Array.prototype.reduce

  • reduce 接受两个参数,第一个为 exc 函数,第二个为初始值,如果不传默认为 0
  • reduce 最终会返回一个值,当然不一定是 Number 类型的,取决于你是怎么计算的,每次的计算结果都会作为下次 exc 中的第一个参数
Array.prototype._reduce=function (exc, initial=0) {
  let result=initial
  this.forEach((item)=> {
    result=exc(result, item)
  })
  return result
}
console.log(b.reduce((pre, cur)=> pre + cur, 0)) // 55
console.log(b._reduce((pre, cur)=> pre + cur, 0)) // 55

Object.create

MDN[1] Object.create() 方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)。

Object.prototype._create=function (proto) {
  const Fn=function () { }
  Fn.prototype=proto
  return new Fn()
}
function A() { }
const obj=Object.create(A)
const obj2=Object._create(A)
console.log(obj.__proto__===A) // true
console.log(obj.__proto__===A) // true

Function.prototype.call

call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。

Function.prototype._call=function (ctx, ...args) {
  // 如果不为空,则需要进行对象包装
  const o=ctx==undefined ? window : Object(ctx)
  // 给 ctx 添加独一无二的属性
  const key=Symbol()
  o[key]=this
  // 执行函数,得到返回结果
  const result=o[key](...args "key")
  // 删除该属性
  delete o[key]
  return result
}

const obj={
  name: '11',
  fun() {
    console.log(this.name)
  }
}

const obj2={ name: '22' }
obj.fun() // 11
obj.fun.call(obj2) // 22
obj.fun._call(obj2) // 22

Function.prototype.bind

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

const obj={
  name: '11',
  fun() {
    console.log(this.name)
  }
}
Function.prototype._bind=function (ctx, ...args) {
  // 获取函数体
  const _self=this
  // 用一个新函数包裹,避免立即执行
  const bindFn=(...reset)=> {
    return _self.call(ctx, ...args, ...reset)
  }
  return bindFn
}
const obj2={ name: '22' }
obj.fun() // 11
const fn=obj.fun.bind(obj2)
const fn2=obj.fun._bind(obj2)
fn() // 22
fn2() // 22

New 关键字

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

const _new=function(constructor) {
  // 创建一个空对象
  const obj={}
  // 原型链挂载
  obj.__proto__=constructor.prototype;
  // 将obj 复制给构造体中的 this,并且返回结果
  const result=constructor.call(obj)
  // 如果返回对象不为一个对象则直接返回刚才创建的对象
  return typeof result==='object' && result !==null ? : result : obj
}

浅拷贝

const _shallowClone=target=> {
  // 基本数据类型直接返回
  if (typeof target==='object' && target !==null) {
    // 获取target 的构造体
    const constructor=target.constructor
    // 如果构造体为以下几种类型直接返回
    if (/^(Function|RegExp|Date|Map|Set)$/i.test(constructor.name)) return target
    // 判断是否是一个数组
    const cloneTarget=Array.isArray(target) ? [] : {}
    for (prop in target) {
      // 只拷贝其自身的属性
      if (target.hasOwnProperty(prop)) {
        cloneTarget[prop]=target[prop]
      }
    }
    return cloneTarget
  } else {
    return target
  }
}

深拷贝

实现思路和浅拷贝一致,只不过需要注意几点

  1. 函数 正则 日期 ES6新对象 等不是直接返回其地址,而是重新创建
  2. 需要避免出现循环引用的情况
const _completeDeepClone=(target, map=new WeakMap())=> {
  // 基本数据类型,直接返回
  if (typeof target !=='object' || target===null) return target
  // 函数 正则 日期 ES6新对象,执行构造题,返回新的对象
  const constructor=target.constructor
  if (/^(Function|RegExp|Date|Map|Set)$/i.test(constructor.name)) return new constructor(target)
  // map标记每一个出现过的属性,避免循环引用
  if (map.get(target)) return map.get(target)
  map.set(target, true)
  const cloneTarget=Array.isArray(target) ? [] : {}
  for (prop in target) {
    if (target.hasOwnProperty(prop)) {
      cloneTarget[prop]=_completeDeepClone(target[prop], map)
    }
  }
  return cloneTarget
}

寄生组合式继承

一图胜千言

function Parent(name) {
  this.name=name
}
Parent.prototype.getName=function () {
  return this.name
}

function Son(name, age) {
  // 这里其实就等于 this.name=name
  Parent.call(this, name)
  this.age=age
}

Son.prototype.getAge=function () {
  return this.age
}
Son.prototype.__proto__=Object.create(Parent.prototype)

const son1=new Son('shao', 20)

console.log(son1.getName()) // shao
console.log(son1.getAge()) // 20

发布订阅者模式

class EventEmitter {
  constructor() {
    // key: 事件名
    // value: callback [] 回调数组
    this.events={}
  }
  on(name, callback) {
    if (this.events[name]) {
      this.events[name].push(callback)
    } else {
      this.events[name]=[callback]
    }
  }
  off(name, callback) {
    if (!this.message[name]) return;
    if (!callback) {
      // 如果没有callback,就删掉整个事件
      this.message[name]=undefined;
    }
    this.message[name]=this.message[name].filter((item)=> item !==callback);

  }
  emit(name, ...args) {
    if (!this.events[name]) return
    this.events[name].forEach(cb=> cb(...args))
  }
}

观察者模式

class Observerd {
  constructor() {
    // 我要看看到底有多少人在观察俺
    this.observerList=[]
  }
  addObserver(observer) {
    // 添加一个观察俺的人
    this.observerList.push(observer)
  }
  notify() {
    // 我要闹点动静,所有观察者都会知道这个信息,具体怎么做就是他们自己的事情了
    this.observerList.forEach(observer=> observer.update())
  }
}


class Observer {
  constructor(doSome) {
    // 观察到小白鼠有动静之后,观察者做的事情
    this.doSome=doSome
  }
  update() {
    console.log(this.doSome)
  }
}

const ob1=new Observer('我是ob1,我观察到小白鼠有反应了,太饿了,我得去吃个饭了')
const ob2=new Observer('我是ob2,我观察到小白鼠有反应了,我要继续工作!')
const xiaoBaiShu=new Observerd()
xiaoBaiShu.addObserver(ob1)
xiaoBaiShu.addObserver(ob2)
xiaoBaiShu.notify() // .... .... 

多说一句:怎么理解发布订阅者和观察者的区别呢 ?

其实发布订阅者模式只有一个中间者,好像啥事情都需要它亲自来做。而且仔细观察的话,发布订阅者模式会存在一个事件名和事件的对应关系,今天可以发布天气预报,只有订阅了天气预报的才会被通知,订阅了 KFC疯狂星期四闹钟事件 的不会被提醒。

而观察者模式,等被观察者发出了一点动静(执行notify),所有观察者都会被通知。

节流

节流函数(throttle)就是让事件处理函数(handler)在大于等于执行周期时才能执行,周期之内不执行,即事件一直被触发,那么事件将会按每小段固定时间一次的频率执行。

function throttle(fn, delay=300) {
  // 这里始终记得字节二面的时候,建议我不写 flag 好家伙
  let isThrottling=false
  // 核心思路,函数多次执行只有当 isThrottling 为 false 时才会进入函数体
  return function (...args) {
    if (!isThrottling) {
      isThrottling=true
      setTimeout(()=> {
        isThrottling=false
        fn.apply(this, args)
      }, delay)
    }
  }
}

防抖

事件响应函数在一段时间后才执行,如果这段时间内再次调用,则重新计算执行时间

function debounce(fn, delay=300) {
  let timer=null
  return function (...args) {
    // 每次进来都会清空定时器,所以在 delay 事件中重复执行之后执行最后一次
    clearInterval(timer)
    timer=setTimeout(()=> {
      fn.apply(this, args)
    }, delay)
  }
}

once 函数

函数返回结果会被缓存下来,只会计算一次。

const f=(x)=> x;
const onceF=once(f);
//=> 3
onceF(3);
//=> 3
onceF(4);
const once=(fn)=> {
  let res, isFirst=true
  return function (...args) {
    if (!isFirst) return res
    res=fn.call(this, ...args)
    isFirst=false
    return res
  }
}

累加函数应用

实现一个累加函数,下面的几种情况都能正确的调用。

console.log(sum(1, 2)(3)()) // 6
console.log(sum(1)(2)(3)()) // 6
console.log(sum(1, 2, 4)(4)()) // 11
function sum(...args) {
  let params=args
  const _sum=(...newArgs)=> {
    if (newArgs.length===0) {
      return params.reduce((pre, cur)=> pre + cur, 0)
    } else {
      params=[...params, ...newArgs]
      return _sum
    }
  }
  return _sum
}

进阶

实现 repeat 方法

function repeat(fn, times, delay) {
  return async function (...args) {
    for (let i=0; i < times; i++) {
      await new Promise((resolve, reject)=> {
        setTimeout(()=> {
          fn.call(this, ...args)
          resolve()
        }, delay)
      })
    }
  }
}
const repeatFn=repeat(console.log, 4, 1000)
// 函数调用四次,每次间隔 1s 打印 hello
repeatFn('hello')

实现 Promise.all/race/allSettled/any

  • Promise 身上的这些方法返回的都是一个 Promise
  • Promise.resolve 接受一个 Promise,若非 promise 则将其变成功状态的 Promise
// 有一个失败则返回失败的结果,全部成功返回全成功的数组
Promise.all=function (promiseList=[]) {
  return new Promise((resolve, reject)=> {
    const result=[]
    let count=0
    if (promiseList.length===0) {
      resolve(result)
      return
    }
    for (let i=0; i < promiseList.length; i++) {
      Promise.resolve(promiseList[i]).then(res=> {
        result[i]=res
        count++
        // 不能直接通过 result.length 进行比较,因为 会存在下标大的先赋值
        // 例如 i=3 第一个返回结果,此时数组变为[empty,empty,empty,res]
        if (count===promiseList.length) {
          resolve(result)
        }
      }).catch(e=> {
        reject(e)
      })
    }
  })
}
// 返回第一个成功或失败的结果
Promise.race=function (promiseList=[]) {
  return new Promise((resolve, reject)=> {
    if (promiseList.length===0) {
      return resolve([])
    }
    for (let i=0; i < promiseList.length; i++) {
      Promise.resolve(promiseList[i]).then(res=> {
        resolve(res)
      }).catch(e=> {
        reject(e)
      })
    }
  })
}
// 无论成功约否都返回,但是会添加一个 status 字段用于标记成功/失败
Promise.allSettled=function (promiseList=[]) {
  return new Promise((resolve, reject)=> {
    const result=[]
    let count=0

    const addRes=(i, data)=> {
      result[i]=data
      count++
      if (count===promiseList.length) {
        resolve(result)
      }
    }
    
    if (promiseList.length===0) return resolve(result)
    for (let i=0; i < promiseList.length; i++) {
      Promise.resolve(promiseList[i]).then(res=> {
        addRes(i, { status: 'fulfilled', data: res })
      }).catch(e=> {
        addRes(i, { status: 'rejected', data: e })
      })
    }
  })
}
// AggregateError,当多个错误需要包装在一个错误中时,该对象表示一个错误。
// 和 Promise.all 相反,全部失败返回失败的结果数组,有一个成功则返回成功结果
Promise.any=function (promiseList=[]) {
  return new Promise((resolve, reject)=> {
    if (promiseList.length===0) return resolve([])
    let count=0
    const result=[]
    for (let i=0; i < promiseList.length; i++) {
      Promise.resolve(promiseList[i]).then(res=> {
        resolve(res)
      }).catch(e=> {
        count++
        result[i]=e
        if (count===promiseList.length) {
          reject(new AggregateError(result))
        }
      })
    }
  })
}

整数千分位加逗号

1234567 -> 1,234,567
function toThousands(num) {
  num=num.toString()
  let result=''
  while (num.length > 3) {
    result=',' + num.substring(num.length - 3) + result
    num=num.substring(0, num.length - 3)
  }
  result=num + result
  return result
}
console.log(toThousands(1234567)) // 1,234,567
console.log(toThousands(123456)) // 123,456

洗牌函数

有几张牌张牌,用 js 来进行乱序排列,要保持公平性

const shuffle=(arr)=> {
  // 不影响原来的数组
  const result=[...arr]
  for (let i=result.length; i > 0; i--) {
    // 随机从 [0,i - 1] 产生一个 index, 将 i - 1 于 index 对应数组的值进行交换
    const index=Math.floor(Math.random() * i);
    [result[index], result[i - 1]]=[result[i - 1], result[index]]
  }
  return result
}
const arr=[1, 2, 3, 4, 5]
console.log(shuffle(arr)) // [ 3, 1, 2, 5, 4 ]
console.log(shuffle(arr)) // [ 2, 3, 5, 1, 4 ]
console.log(shuffle(arr)) // [ 4, 2, 3, 1, 5 ]
console.log(shuffle(arr)) // [ 5, 4, 2, 3, 1 ]

a==1 && a==2 && a==3

如何让 a==1 && a==2 && a==3 返回 true 呢

方案一

利用隐式转换会调用 valueOf

const a={
  value: 1,
  valueOf() {
    return this.value++
  }
}
console.log(a==1 && a==2 && a==3) // true

方案二

在对象 valueOf 函数不存在的情况下会调用 toString 方法

const a={
  value: 1,
  toString() {
    return this.value++
  }
}

console.log(a==1 && a==2 && a==3) // true

方案三

利用Object.defineProperty 在全局 window 上挂载一个 a 属性

let _a=1
Object.defineProperty(window, 'a', {
  get() {
    return _a++
  }
})

console.log(a==1 && a==2 && a==3)

手写LRU

LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法[2],选择最近最久未使用的页面予以淘汰。该算法赋予每个页面[3]一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。

力扣地址[4]

/**
 * @param {number} capacity
 */
var LRUCache=function(capacity) {
    this.map=new Map()
    this.capacity=capacity
};

/** 
 * @param {number} key
 * @return {number}
 */
LRUCache.prototype.get=function(key) {
    if(this.map.has(key)){
        const value=this.map.get(key)
        // 更新存储位置
        this.map.delete(key)
        this.map.set(key,value)
        return value
    }
    return - 1
};

/** 
 * @param {number} key 
 * @param {number} value
 * @return {void}
 */
LRUCache.prototype.put=function(key, value) {
    if(this.map.has(key)){
        this.map.delete(key)
    }
    this.map.set(key,value)
    // 如果此时超过了最长可存储范围
    if(this.map.size > this.capacity){
        // 删除 map 中最久未使用的元素
        this.map.delete(this.map.keys().next().value)
    }
};

优化一下

更上一层楼

Generator

先看看下面输出的内容

async function getResult() {
    await new Promise((resolve, reject)=> {
        setTimeout(()=> {
            resolve(1);
            console.log(1);
        }, 1000);
    })
    await new Promise((resolve, reject)=> {
        setTimeout(()=> {
            resolve(2);
            console.log(2);
        }, 500);
    })
    await new Promise((resolve, reject)=> {
        setTimeout(()=> {
            resolve(3);
            console.log(3);
        }, 100);
    })

}
getResult()
// 1 2 3 

那如何使用 Es6 中的 generator 实现类似的效果呢 ?

function* getResult(params) {
    yield new Promise((resolve, reject)=> {
        setTimeout(()=> {
            resolve(1);
            console.log(1);
        }, 1000);
    })
    yield new Promise((resolve, reject)=> {
        setTimeout(()=> {
            resolve(2);
            console.log(2);
        }, 500);
    })
    yield new Promise((resolve, reject)=> {
        setTimeout(()=> {
            resolve(3);
            console.log(3);
        }, 100);
    })
}
const gen=getResult()
// gen.next().value 就是每一次 yield 之后返回的 Promise
// gen.next()={value: yeild 返回的数据,done: 迭代器是否走完}
gen.next().value.then(()=> {
    gen.next().value.then(()=> {
        gen.next();
    });
});// 依次打印 1 2 3

将 gen.next() 封装一层,让其自己能够实现递归调用

const gen=getResult()
function co(g) {
  const nextObj=g.next();
  // 递归停止条件:当迭代器迭代到最后一个 yeild 
  if (nextObj.done) {
    return;
  }
  nextObj.value.then(()=>{
    co(g)
  })
}
co(gen)

async-pool

JS 控制并发请求, 参考文章 mp.weixin.qq.com/s/yWOPoef9ixuSBWApZQhjIg

aysnc-pool 的基本使用

const timeout=i=> new Promise(resolve=> setTimeout(()=> resolve(i), i));
await asyncPool(2, [1000, 5000, 3000, 2000], timeout);

asyncPool 这个函数接受三个参数

  • poolLimit(数字类型):表示限制的并发数;
  • array(数组类型):表示任务数组;
  • iteratorFn(函数类型):表示迭代函数,用于实现对每个任务项进行处理,该函数会返回一个 Promise 对象或异步函数。

这里提醒一下,promise.then 中的函数执行是一异步的,而赋值是同步的

const a=Promise.resolve().then(()=>console.log(a))
// 等价于 此时 a 等于一个 pending 状态的 promise
const a=Promise.resolve().then()
a.then(()=>{
  console.log(a)
})

手写实现,这部分可能会多花点时间。可以拷贝代码多调试几次就知道了

async function asyncPool(poolLimit, array, iteratorFn) {
  const ret=[]; // 存储所有的异步任务
  const executing=[]; // 存储正在执行的异步任务
  for (const item of array) {
    // 调用iteratorFn函数创建异步任务
    const p=Promise.resolve().then(()=> iteratorFn(item, array));
    ret.push(p); // 保存新的异步任务

    // 当poolLimit值小于或等于总任务个数时,进行并发控制
    if (poolLimit <=array.length) {
      // 当任务完成后,从正在执行的任务数组中移除已完成的任务
      const e=p.then(()=> executing.splice(executing.indexOf(e), 1));
      executing.push(e); // 保存正在执行的异步任务
      if (executing.length >=poolLimit) {
        await Promise.race(executing); // 等待较快的任务执行完成
      }
    }
  }
  return Promise.all(ret);
}

const timeout=i=> new Promise(resolve=> setTimeout(()=> { console.log(i); resolve(i) }, i));
// 当然,limit <=0 的时候 我们可以理解为只允许一个请求存在 
asyncPool(2, [1000, 5000, 3000, 2000], timeout).then(res=> {
  console.log(res)
})

总共花费 6 s 时间,符合预期

总结

收集这些手写的时候,自己也学到了很多东西,其实也是一个查漏补缺的过程。感谢你看到这里,点赞收藏 offer ++

源:中国网2015-04-01

存款保险制度的确立,以及利率市场化加速,这对于互联网金融、民营银行的良性发展意义重大。将来一部分管理能力差、经营不稳定、竞争力不强的银行就可能会被退出市场,或被整合和收购。

作者:王振峰

酝酿20多年的存款保险制度终于出炉了。近日,国务院正式公布《存款保险条例》,并将于今年5月1日起正式实施。(3月31日 中国网)

《条例》称,建立和规范存款保险制度,是为了“依法保护存款人的合法权益,及时防范和化解金融风险,维护金融稳定”。《条例》共计二十三条,其中强制规定所有的银行业金融机构,都必须依规投保存款保险。存款保险实行限额偿付,目前最高偿付50万元。此外,《条例》还实行差别费率,有观点认为,这将降低存款机构“道德风险”。

今年全国两会答记者问时,国务院总理李克强就明确表示,今年,我们就要出台存款保险制度。他还指出,“我们允许个案性金融风险的发生,按市场化的原则进行清算,这是为了防止道德风险,也增强人们的风险意识”。而两会期间人民银行行长周小川的答问中,把存款保险制度推出的时间更具体一些;他表示,存款保险制度作为金融改革重要的一步棋,各方面条件已经基本成熟,“我个人估计,今年上半年就可以出台”。

对于推出存款保险制度的意义,普遍的观点认为,这将有助于金融改革迈入深水区,我国金融改革整体推进已是箭在弦上。作为当前金融改革的重要环节,存款保险制度的建立,以及允许银行破产制度,体现了新一轮金融改革重在制度构建、重在完善市场、重在机制形成、重在资源配置与功能优化。

引入并推出存款保险制度,是存款利率自由浮动的先决条件;可以说存款保险一“吹号”,利率市场化“在路上”!建立金融机构破产退出机制,建立存款保险制度,可视为进行利率市场化建设的最重要配套制度建设。当然,存保制度之下,也将意味着今后那些经营不善的银行可能会破产、被清算。存款保险制度的建立,也就意味打破了过去的“刚兑”机制,银行一旦倒闲,国家再也不兜底了。普通老百姓应准备树立银行存储及各种理财的风险意识:“鸡蛋不要放在一个篮子里”。

建立存款保险制度,主要也在于防范系统性风险,而坚持市场化导向,进一步释放我国金融体系活力,建成起完善的金融机构体系、金融市场体系、金融运行体系和金融监管体系,使金融更好地为实体经济服务,也是这个制度推行的重要出发点。存款保险制度的确立,以及利率市场化加速,这对于互联网金融、民营银行的良性发展意义重大。

最高偿付限额50万元,银行间存款分布可能比以前相对平衡,储户也可能会更多元化的投资,互联网金融和民营银行的发展也将更有空间和余地。尤其是出台存款保险制度,这无疑为发展民营银行扫清了障碍。在政策鼓励金融行业向民营资本开放的背景下,或许民营资本进入银行业的春天已经不远了。

今后,银行体系内部之间、金融机构与非金融机构间、以及互联网金融与传统金融机构之间,谁能在竞争中脱颖而出,将更多地由市场来决定。这一方面有利于激活金融市场活力,有利于金融创新,进一步增强金融服务实体经济的能力;另一方面,普通老百姓也因为这种市场竞争得到更优的金融服务,也可以有更多的选择和更好的收益机会。当然,存款保险建立以后,银行业金融机构因强制投保,运营成本也将提高不少,利率市场化也将促进银行业洗牌,将来一部分管理能力差、经营不稳定、竞争力不强的银行就可能会被退出市场,或被整合和收购。如此来看,随着存款保险制度的推行,银行洗牌时代也将来临。

http://opinion.china.com.cn/opinion_82_125982.html