JavaScript 中有很多值得学习记录的小技巧和经典的代码片段,在这里做个小总结。

1. 注意字符串连接

JavaScript 中经常会有一些意想不到的类型转换,+ 是其中最常见的一种运算,它既可以做数字的加法,也可以做字符串连接,不注意使用可能会出现不想看到的结果。看下面的例子

const c1 = 12, c2 = 34, c3 = '56'

const result1 = c1 + c2 + c3 // '4656'
const result2 = ''.concat(c1, c2, c3) // '123456'
/* 当然也可以这样,总是在前面加个'' */
const result3 = '' + c1 + c2 + c3 // '123456'

结果 1 中先做了加法,而非我们期望的字符串连接,使用后两种方法更安全一些。 如果是 ES6,使用 template literals 显然更方便。

const c1 = 12, c2 = 34, c3 = '56'

const result = `${c1}${c2}${c3}` // '123456'

2. 检查某个数是否为-1

要知道 if(n) 只有当 n (数字类型)为 0 时才会判定为 false,而对于 ~n 只有当 n 为 -1 时会得到 0,这在使用 Array.prototype.indexOf() 判断某一项是否在数组中时可以起到简化代码的作用,如下

const a = [1, 2, 3]
if (a.indexOf(4) !== -1) { // 或者 a.indexOf(4) > -1
  // 数组 a 中含有 4
}

使用 ~ 我们可以像下面这样,代码会更简洁,只是可读性会稍稍降低

const a = [1, 2, 3]
if (~a.indexOf(4)) {
  // 数组 a 中含有 4
}

3. 双波浪运算(~~)

~~ 表示执行两次 ~ (按位非)运算,对于正数,它相当于 Math.floor(),对于负数则相当于 Math.ceil(),只是性能更高,写起来也更快,不过要注意它只能用于 32 位及以内的数值。

~12       // -13
~~12.34     // 12
~~12.89     // 12
~~-12.88    // -12

4. 使用 !! 总是返回 Boolean 类型

!! 表示执行两次非运算,经过类型转换之后总是会返回布尔值,这在有些场景下会很有用。

// 判断 value 是否为布尔值
function isBoolean (value) {
  return value === !!value
}

5. Debouncing VS Throttling

Debouncing 和 Throttling 的作用都是防止某个函数执行过于频繁,以提高性能。不同的是,Debouncing 表示把某一段时间内重复触发的事件归结到一次回调中执行,比如谷歌或百度的动态搜索框的文本输入事件,可以设置在键盘输入停止 500ms 才发起 ajax 请求;而 Throttling 表示的是在某一时间段内,限制某一函数只执行一次,比如一个动态加载的列表,它会在滚动到底部时加载新的列表项,然而用户的滚动事件的触发太频繁,我们可以限制它每 500ms 最多执行一次加载操作。更详细的说明可以参考这篇文章underscorelodash 都有对这两个高阶函数的实现和拓展,不考虑拓展的基本实现如下:

const debounce = function (fn, delay) {
  let timer
  return function () {
    const args = arguments,
      self = this
    if (timer) {
      clearTimeout(timer)
    }
    timer = setTimeout(function () {
      clearTimeout(timer)
      fn.apply(self, args)
      timer = null
    }, delay)
  }
}
const throttle = function (fn, interval) {
  let timer,
    firstTime = true // 是否首次调用
  return function () {
    const args = arguments,
      self = this
    if (firstTime) { // 如果是首次调用,不用延时
      fn.apply(self, args)
      return firstTime = false
    }
    if (timer) {
      return false
    }
    timer = setTimeout(function () {
      clearTimeout(timer)
      fn.apply(self, args)
      timer = null
    }, interval)
  }
}

6. 惰性载入函数

由于浏览器之间的差异,在开发前端脚本的时候,一些特性嗅探操作总是不可避免的,比如一个比较通用的添加事件的函数

const addListener = function (el, type, handler) {
  if (window.addEventListener) {
    el.addEventListener(type, handler, false)
  } else {
    el.attachEvent(type, handler)
  }
}

上面代码的缺点是每次调用 addListener 都用执行里面的 if 分支判断,这种情况是完全可以避免的

let addListener = function (el, type, handler) {
  if (window.addEventListener) {
    addListener = function (el, type, handler) {
      el.addEventListener(type, handler, false)
    }
  } else {
    addListener = function (el, type, handler) {
      el.attachEvent(type, handler)
    }
  }
  addListener(el, type, handler)
}

上面的代码直接在函数内部重写了 addListener 函数,这样只有首次调用的时候需要做 if 判断,往后的调用都不需要再做判断。

7. html文本编码

对于插入 DOM 中的文本,我们需要对 <>&" 等特殊字符进行转义,通常可以像下面这样子做:

function htmlEscape (test) {
  return text.replace(/[<>&"]/g, function (match) {
    switch (match) {
      case '<':
        return '&lt;'
      case '>':
        return '&gt;'
      case '&':
        return '&amp;'
      case '"':
        return '&quot;'
    }
  })
}

其实在浏览器环境中,还可以写成下面那样:

function htmlEscape (text) {
  return document.createElement('p').appendChild(document.createTextNode(text)).parentNode.innerHTML
}

8. 简单的类型判断

例如判断一个变量是否为数组?只是使用 instanceof 的话在多个 iframe 的页面,由于不同 iframe 全局执行环境不同,存在多个不同的 Array 构造函数,不一定可行;在 es5 中可以直接使用静态方法 Array.isArray(),而比较通用的方法则如下所示:

function isArray (value) {
  return Object.prototype.toString.call(value) === '[object Array]'
}

当然除了数组,JavaScript 中的基本类型和其他内置对象也可以用这种方法。

function isNumber (value) {
  return Object.prototype.toString.call(value) === '[object Number]'
}

9. 检测是否为浏览器内部对象

function isNative (constructor) {
  return /native code/.test(constructor.toString())
}

比如检测当前作用域下的 Promise 构造器是否为浏览器自带的 Promise 实现。

isNative(Promise)