循环/枚举/递归/尾调用/尾递归/函数式编程/函数缓存/异步编程

211 阅读9分钟

Object与Map有什么区别?

  • Object

在ECMAScript中,Object是一个特殊的对象。它本身是一个顶级对象,同时还是一个构造函数,可以通过它(如:new Object())来创建一个对象。我们可以认为JavaScript中所有的对象都是Object的一个实例,对象可以用字面量的方法const obj = {}即可声明。

  • Map

Map是Object的一个子类,可以有序保存任意类型的数据,使用键值对去存储,其中键可以存储任意类型,通过const m = new Map();即可得到一个map实例。

访问

map: 通过map.get(key)方法访问属性, 不存在则返回undefined

object: 通过obj.a或者obj['a']去访问一个属性, 不存在则返回undefined

赋值

map: 通过map.set去设置一个值,key可以是任意类型

object: 通过object.a = 1或者object['a'] = 1,去赋值,key只能是字符串,数字或symbol

删除

map: 通过map.delete去删除一个值,试图删除一个不存在的属性会返回false

object: 通过delete操作符才能删除对象的一个属性,诡异的是,即使对象不存在该属性,删除也返回true,当然可以通过Reflect.deleteProperty(target, prop)  删除不存在的属性还是会返回true。

var obj = {}; // undefined
delete obj.a // true

大小

map: 通过map.size即可快速获取到内部元素的总个数

object: 需要通过Object.keys的转换才能将其转换为数组,再通过数组的length方法去获得或者使用Reflect.ownKeys(obj)也可以获取到keys的集合

迭代

map: 拥有迭代器,可以通过for-of forEach去直接迭代元素,且遍历顺序是确定的

object: 并没有实现迭代器,需要自行实现,不实现只能通过for-in循环去迭代,遍历顺序是不确定的

使用场景

  1. 如果只需要简单的存储key-value的数据,并且key不需要存储复杂类型的,直接用对象
  2. 如果该对象必须通过JSON转换的,则只能用对象,目前暂不支持Map
  3. map的阅读性更好,所有操作都是通过api形式去调用,更有编程体验

forEach中return有效果吗?如何中断forEach循环?

在forEach中用return不会返回,函数会继续执行。

中断方法

  • 使用try监视代码块,在需要中断的地方抛出异常。
  • 官方推荐方法(替换方法):用every和some替代forEach函数。
    • every在碰到return false的时候,中止循环。
    • some在碰到return true的时候,中止循环。

for...in和for...of有什么区别?

for…of 是ES6新增的遍历方式,允许遍历一个含有iterator接口的数据结构(数组、对象等)并且返回各项的值,和ES3中的for…in的区别如下:

  • for…of 遍历获取的是对象的键值,for…in 获取的是对象的键名;
  • 而 for … of 只遍历当前对象不会遍历原型链,for… in 会遍历对象的整个原型链,性能非常差不推荐使用,
  • 对于数组的遍历,for…in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for…of 只返回数组的下标对应的属性值; 总结: for...in 循环主要是为了遍历对象而生,不适用于遍历数组;for...of 循环可以用来遍历数组、类数组对象,字符串、Set、Map 以及 Generator 对象。

js对象中,可枚举性(enumerable)是什么?

可枚举性(enumerable)用来控制所描述的属性,是否将被包括在for...in循环之中(除非属性名是一个Symbol)。具体来说,如果一个属性的enumerable为false,下面三个操作不会取到该属性。
for..in循环
Object.keys方法
JSON.stringify方法

var o = { a: 1, b: 2 };

o.c = 3;
Object.defineProperty(o, "d", {
  value: 4,
  enumerable: false,
});

o.d;
// 4

for (var key in o) console.log(o[key]);
// 1
// 2
// 3

Object.keys(o); // ["a", "b", "c"]

JSON.stringify(o); // => "{a:1,b:2,c:3}"

上面代码中,d属性的enumerable为false,所以一般的遍历操作都无法获取该属性,使得它有点像“秘密”属性,但还是可以直接获取它的值。 至于for...in循环和Object.keys方法的区别,在于for...in包括对象继承自原型对象的属性,而后者只包括对象本身的属性。如果需要获取对象自身的所有属性,不管enumerable的值,可以使用Object.getOwnPropertyNames方法。

var o = { a: 1, b: 2 };

o.c = 3;
Object.defineProperty(o, "d", {
  value: 4,
      enumerable: false, // Object.defineProperty定义的属性,其实enumerable的默认值就是false
});
Object.getOwnPropertyNames(o) // ['a', 'b', 'c', 'd']

可枚举属性是指那些内部 “可枚举” 标志设置为 true 的属性。对于通过直接的赋值和属性初始化的属性,该标识值默认为即为 true。但是对于通过 Object.defineProperty 等定义的属性,该标识值默认为 false

尾调用

某个函数的最后一步是调用另一个函数。

image.png

以下两种情况,都不属于尾调用。

image.png

上面代码中,情况一是调用函数g之后,还有别的操作,所以不属于尾调用,即使语义完全一样。 情况二也属于调用后还有操作,即使写在一行内。
**尾调用不一定出现在函数尾部,只要是最后一步操作即可。**

function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

上面代码中,函数m和n都属于尾调用,因为它们都是函数f的最后一步操作。

尾调用优化

尾调用之所以与其他调用不同,就在于它的特殊的调用位置。
我们知道,函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。

image.png

上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除 f() 的调用记录,只保留 g(3) 的调用记录。(如果调用后面还有其他操作,也就是不属于尾调用,那么外层函数的调用记录就没法删除,同时要包含外层函数以及内层函数的调用记录会造成栈溢出
这就叫做"尾调用优化"(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。

尾递归

函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

递归非常耗费内存,因为需要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误。节省内存 image.png

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120

上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。 如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。


function factorial(n, total) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

factorial(5, 1) // 120

factorial(4, 5*1)
factorial(3, 4*5*1)
factorial(2, 3*4*5*1)
factorial(1, 2*3*4*5*1) -> return 2*3*4*5*1

数组求和

function sumArray(arr, total = 0) {
  if(arr.length === 0) {
      return total
  }
  return sumArray(arr, total + arr.pop())
}

使用尾递归优化求斐波那契数列

function factorial2 (n, start = 1, total = 1) {
  if(n <= 2){
      return total
  }
  return factorial2 (n -1, total, total + start)
}

数组扁平化

let a = [1,2,3, [1,2,3, [1,2,3]]]
// 变成
let a = [1,2,3,1,2,3,1,2,3]
// 具体实现
function flat(arr = [], result = []) {
    arr.forEach(v => {
        if(Array.isArray(v)) {
            result = result.concat(flat(v, []))
        }else {
            result.push(v)
        }
    })
    return result
}

数组对象格式化


let obj = {
  a: '1',
  b: {
      c: '2',
      D: {
          E: '3'
      }
  }
}
// 转化为如下:
let obj = {
  a: '1',
  b: {
      c: '2',
      d: {
          e: '3'
      }
  }
}

// 代码实现
function keysLower(obj) {
  let reg = new RegExp("([A-Z]+)", "g");
  for (let key in obj) {
      if (obj.hasOwnProperty(key)) { // 判断对象是否包含特定的自身(非继承)属性。
          let temp = obj[key];
          if (reg.test(key.toString())) {
              // 将修改后的属性名重新赋值给temp,并在对象obj内添加一个转换后的属性
              temp = obj[key.replace(reg, function (result) {
                  return result.toLowerCase()
              })] = obj[key];
              // 将之前大写的键属性删除
              delete obj[key];
          }
          // 如果属性值是对象或者数组,重新执行函数
          if (typeof temp === 'object' || Object.prototype.toString.call(temp) === '[object Array]') {
              keysLower(temp);
          }
      }
  }
  return obj;
};

函数式编程

image.png

函数缓存

image.png

异步编程有哪些实现方式?

js 中的异步机制可以分为以下几种:

  1. 回调函数的方式 使用回调函数的方式有一个缺点是,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数间的代码耦合度太高,不利于代码的可维护。\
  2. Promise方式 使用 Promise 的方式可以将嵌套的回调函数作为链式调用。但是使用这种方法,有时会造成多个 then 的链式调用,可能会造成代码的语义不够明确。\
  3. generator方式 当我们遇到异步函数执行的时候,将函数执行权转移出去,当异步函数执行完毕的时候我们再将执行权给转移回来。因此我们在 generator 内部对于异步操作的方式,可以以同步的顺序来书写。使用这种方式我们需要考虑的问题是何时将函数的控制权转移回来,因此我们需要有一个自动执行 generator 的机制,比如说 co 模块等方式来实现 generator 的自动执行。\
  4. async 函数的形式 async 函数是 generator 和 promise 实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个 await 语句的时候,如果语句返回一个 promise 对象,那么函数将会等待 promise 对象的状态变为 resolve 后再继续向下执行。因此我们可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。