# typeof

typeof 可以正确识别:Undefined、Boolean、Number、String、Symbol、Function 等类型的数据,但是对于其他的都会认为是 object,比如 Null、Date 等,所以通过 typeof 来判断数据类型会不准确。但是可以使用 Object.prototype.toString 实现。

function _typeOf(obj) {
  return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase()
}

_typeOf()          // 'undefined'
_typeOf([])        // 'array'
_typeOf({})        // 'object'
_typeOf(new Date)  // 'date'
1
2
3
4
5
6
7
8

# instanceof

instanceof 运算符用于检测构造函数的 prototype 对象是否出现在某个实例对象的原型链上。

function _instanceof(L, R) {
  const proto = R.prototype
  L = L.__proto__

  while(true) {
    if (L === null) return false
    if (L === proto) return true
    L = L.__proto__
  }
}
1
2
3
4
5
6
7
8
9
10

# new 操作符

new 做了什么:

  1. 创建一个空的简单 JavaScript 对象(即{});
  2. 为这个新创建的对象添加属性__proto__,并指向构造函数的原型对象 ;
  3. 将 this 指向新创建的对象;
  4. 执行构造函数内的代码(为新对象添加属性)
  5. 如果该函数没有返回对象,则返回新对象。

代码实现:

function _new(Ctor) {
  if (typeof Ctor !== 'function') {
    throw new TypeError(Ctor + ' is not a constructor')
  }
  // es6
  _new.target = Ctor
  const obj = Object.create(Ctor.prototype) // 步骤 1,2,4
  const args = [].slice(arguments, 1)
  const result = Ctor.apply(obj, args) // 步骤 3
  const isObject = result !== null && typeof result === 'object'
  const isFunction = typeof result === 'function'
  if (isObject || isFunction) { // 步骤 5
    return result
  } else {
    return obj
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

参考:能否模拟实现 JS 的 new 操作符 — 若川 (opens new window)

# apply,call,bind 三者的区别

  • 三者都可以改变函数的 this 对象指向。
  • 三者第一个参数都是 this 要指向的对象,如果如果没有这个参数或参数为 undefined 或 null,则默认指向全局 window。
  • 三者都可以传参,但是 apply 是数组,而 call 是参数列表,且 apply 和 call 是一次性传入参数,而 bind 可以分为多次传入。
  • bind 是返回绑定 this 之后的函数,便于稍后调用;apply 、call 则是立即执行 。

# apply

# call

# bind

  • 修改 this 指向为第一个参数
  • 参数可以传多个,且可以多次传入
  • 返回一个函数,函数带有初始值和参数
Function.prototype._bind = function () {
  const args = Array.from(arguments)
  const context = args.shift()
  const self = this
  return function () {
    return self.apply(context, args)
  }
}

// test
function fn1(a, b, c) {
  console.log(this)
  console.log(a, b, c)
  return 'fn1'
}
const fn2 = fn1._bind({x:100}, 1, 2)
console.log(fn2())
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# Promise

# 调度器

class Scheduler {
  constructor () {
    this.task = []
    this.curringRuning = 0
  }

  add (promiseCreator) {
    return new Promise((resolve) => {
      this.task.push(() => promiseCreator().then(() => resolve()))
      // 控制最多执行两个
      if (this.curringRuning < 2) this.doTask()
    })
  }

  doTask () {
    if (this.task.length > 0) {
      const runTask = this.task.shift()
      this.curringRuning++
      runTask().then(() => { // 完成 1 个后,开始下一个,保证最多执行 2 个
        this.curringRuning--
        this.doTask()
      })
    }
  }
}

module.exports = Scheduler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# deepClone

浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

简单版(仅支持数组和对象):

const deepClone = obj => {
  if (obj === null) return null;
  let clone = Object.assign({}, obj);
  Object.keys(clone).forEach(
    key =>
      (clone[key] =
        typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key])
  );
  if (Array.isArray(obj)) {
    clone.length = obj.length;
    return Array.from(clone);
  }
  return clone;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# EventEmitter

# JSONP

是什么:jsonp(JSON with Padding)是 json 的一种"使用方法",不是一种单独的技术。

原理:利用 script 标签的 src 没有跨域限制来完成跨域目的。

过程:

  • 前端定义一个解析函数:const jsonpCallBackFn = function (res) {}
  • 通过 params 的形式包装 script 标签的请求参数,并且声明执行函数,如:cb = jsonpCallBackFn
  • 后端获取前端声明的执行函数(jsonpCallBackFn),并且带上参数且通过调用函数的方式传递给前端;
  • 前端在 script 标签返回资源的时候就会执行 jsonpCallBackFn,后端返回的数据就填充(padding)在 jsonpCallBackFn 的参数之中。

例子

  • 如客户想访问:https://www.baidu.com/jsonp.php?cb=jsonpCallBackFn
  • 假设客户期望返回数据:["data1","data2"]
  • 真正返回到客户端的数据显示为:jsonpCallBackFn(["data1","data2"])
  • jsonp 中的p,就是 padding(填充) 的意思,把数据填充到了函数的参数位置。

优点:兼容性好,低版本的浏览器中可使用;

缺点:只能进行 get 请求;

代码实现

// 简单版本 - 只能进行一次请求
// 调用多次时,因为 callbackName 相同,后一个的覆盖掉前面的。
function JSONP ({
  url,
  params = {},
  callbackKey = 'cb',
  callback
}) {
  // 定义本地的一个 callback 的名称
  const callbackName = 'jsonpCallback';
  // 把这个名称加入到参数中:'cb=jsonpCallback'
  params[callbackKey] = callbackName;
  //  把这个 callback 加入到 window 对象中,这样就能执行这个回调了
  window[callbackName] = callback;

  // 得到'id=1&cb=jsonpCallback'
  const paramString = Object.keys(params).map(key => {
    return `${key}=${encodeURIComponent(params[key])}`
  }).join('&')
  // 创建 script 标签
  const script = document.createElement('script');
  script.setAttribute('src', `${url}?${paramString}`);
  document.body.appendChild(script);
}

JSONP({
  url: 'http://localhost:8080/api/jsonp',
  params: { id: 1 },
  callbackKey: 'cb',
  callback (res) {
    console.log(res)
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 完整版 - 可多次调用
// 让 callbackName 保持唯一,可以使用 id 递增的方式
// 把回调定义在 JSONP.callbacks 数组上,避免污染全局环境
function JSONP ({
  url,
  params = {},
  callbackKey = 'cb',
  callback
}) {
  // 定义本地的唯一 callbackId,若是没有的话则初始化为 1
  JSONP.callbackId = JSONP.callbackId || 1;
  let callbackId = JSONP.callbackId;
  // 把要执行的回调加入到 JSON 对象中,避免污染 window
  JSONP.callbacks = JSONP.callbacks || [];
  JSONP.callbacks[callbackId] = callback;
  // 把这个名称加入到参数中:'cb=JSONP.callbacks[1]'
  params[callbackKey] = `JSONP.callbacks[${callbackId}]`;

  // 得到'id=1&cb=JSONP.callbacks[1]'
  const paramString = Object.keys(params).map(key => {
    return `${key}=${encodeURIComponent(params[key])}`
  }).join('&')

  // 创建 script 标签
  const script = document.createElement('script');
  script.setAttribute('src', `${url}?${paramString}`);
  document.body.appendChild(script);

  // id 自增,保证唯一
  JSONP.callbackId++;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

参考:JSONP 原理及实现 - 简书 (opens new window)

# Ajax

一个完整的 AJAX 请求一般包括以下步骤:

  • 实例化 XMLHttpRequest 对象
  • 连接服务器
  • 发送请求
  • 接收响应数据
const getJSON = function(url) {
  return new Promise((resolve, reject) => {
    const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp');
    xhr.open('GET', url, false);
    xhr.setRequestHeader('Accept', 'application/json');
    xhr.onreadystatechange = function() {
      if (xhr.readyState !== 4) return;
      if (xhr.status === 200 || xhr.status === 304) {
        resolve(xhr.responseText);
      } else {
        reject(new Error(xhr.responseText));
      }
    }
    xhr.send();
  })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 数组去重

ES5 实现:

function unique(arr) {
    var res = arr.filter(function(item, index, array) {
        return array.indexOf(item) === index
    })
    return res
}
1
2
3
4
5
6

ES6 实现:

var unique = arr => [...new Set(arr)]
1

# 数组扁平化

数组扁平化就是将 [1, [2, [3]]] 这种多层的数组拍平成一层 [1, 2, 3]。使用 Array.prototype.flat 可以直接将多层数组拍平成一层:

[1, [2, [3]]].flat(2)  // [1, 2, 3]
1

现在就是要实现 flat 这种效果。

ES5 实现:递归。

function flatten(arr) {
    var result = [];
    for (var i = 0, len = arr.length; i < len; i++) {
        if (Array.isArray(arr[i])) {
            result = result.concat(flatten(arr[i]))
        } else {
            result.push(arr[i])
        }
    }
    return result;
}
1
2
3
4
5
6
7
8
9
10
11

ES6 实现:

function flatten(arr) {
    while (arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr);
    }
    return arr;
}
1
2
3
4
5
6

# 深浅拷贝

浅拷贝:只考虑对象类型。

function shallowCopy(obj) {
    if (typeof obj !== 'object') return

    let newObj = obj instanceof Array ? [] : {}
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = obj[key]
        }
    }
    return newObj
}
1
2
3
4
5
6
7
8
9
10
11

简单版深拷贝:只考虑普通对象属性,不考虑内置对象和函数。

function deepClone(obj) {
    if (typeof obj !== 'object') return;
    var newObj = obj instanceof Array ? [] : {};
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key];
        }
    }
    return newObj;
}
1
2
3
4
5
6
7
8
9
10

复杂版深克隆:基于简单版的基础上,还考虑了内置对象比如 Date、RegExp 等对象和函数以及解决了循环引用的问题。

const isObject = (target) => (typeof target === "object" || typeof target === "function") && target !== null;

function deepClone(target, map = new WeakMap()) {
    if (map.get(target)) {
        return target;
    }
    // 获取当前值的构造函数:获取它的类型
    let constructor = target.constructor;
    // 检测当前对象 target 是否与正则、日期格式对象匹配
    if (/^(RegExp|Date)$/i.test(constructor.name)) {
        // 创建一个新的特殊对象(正则类/日期类)的实例
        return new constructor(target);  
    }
    if (isObject(target)) {
        map.set(target, true);  // 为循环引用的对象做标记
        const cloneTarget = Array.isArray(target) ? [] : {};
        for (let prop in target) {
            if (target.hasOwnProperty(prop)) {
                cloneTarget[prop] = deepClone(target[prop], map);
            }
        }
        return cloneTarget;
    } else {
        return target;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

# 事件总线(发布订阅模式)

class EventEmitter {
    constructor() {
        this.cache = {}
    }
    on(name, fn) {
        if (this.cache[name]) {
            this.cache[name].push(fn)
        } else {
            this.cache[name] = [fn]
        }
    }
    off(name, fn) {
        let tasks = this.cache[name]
        if (tasks) {
            const index = tasks.findIndex(f => f === fn || f.callback === fn)
            if (index >= 0) {
                tasks.splice(index, 1)
            }
        }
    }
    emit(name, once = false, ...args) {
        if (this.cache[name]) {
            // 创建副本,如果回调函数内继续注册相同事件,会造成死循环
            let tasks = this.cache[name].slice()
            for (let fn of tasks) {
                fn(...args)
            }
            if (once) {
                delete this.cache[name]
            }
        }
    }
}

// 测试
let eventBus = new EventEmitter()
let fn1 = function(name, age) {
	console.log(`${name} ${age}`)
}
let fn2 = function(name, age) {
	console.log(`hello, ${name} ${age}`)
}
eventBus.on('aaa', fn1)
eventBus.on('aaa', fn2)
eventBus.emit('aaa', false, '布兰', 12)
// '布兰 12'
// 'hello, 布兰 12'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

# 解析 URL 参数为对象

function parseParam(url) {
    const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
    const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中
    let paramsObj = {};
    // 将 params 存到对象中
    paramsArr.forEach(param => {
        if (/=/.test(param)) { // 处理有 value 的参数
            let [key, val] = param.split('='); // 分割 key 和 value
            val = decodeURIComponent(val); // 解码
            val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字

            if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
                paramsObj[key] = [].concat(paramsObj[key], val);
            } else { // 如果对象没有这个 key,创建 key 并设置值
                paramsObj[key] = val;
            }
        } else { // 处理没有 value 的参数
            paramsObj[param] = true;
        }
    })

    return paramsObj;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 函数防抖(debounce)

其实是从机械开关和继电器的“去弹跳”(debounce)衍生出来的,基本思路就是把多个信号合并为一个信号。

触发高频事件 N 秒后只会执行最后一次,如果 N 秒内事件再次触发,则会重新计时。

常见场景:input 输入、按钮提交。

简单版:函数内部支持使用 this 和 event 对象;

function debounce(fn, wait) {
  const timer
  return function () {
    const context = this
    const args = arguments
    clearTimeout(timer)
    timer = setTimeout(function(){
      fn.apply(context, args)
    }, wait)
  }
}
1
2
3
4
5
6
7
8
9
10
11

使用:

const node = document.getElementById('layout')
function getUserAction(e) {
  console.log(this, e)  // 分别打印:node 这个节点 和 MouseEvent
  node.innerHTML = count++;
};
node.onmousemove = debounce(getUserAction, 1000)
1
2
3
4
5
6

最终版:除了支持 this 和 event 外,还支持以下功能:

  • 支持立即执行;
  • 函数可能有返回值;
  • 支持取消功能;
function debounce(func, wait, immediate) {
  const timeout, result;

  const debounced = function () {
    const context = this;
    const args = arguments;

    if (timeout) clearTimeout(timeout);
    if (immediate) {
      // 如果已经执行过,不再执行
      const callNow = !timeout;
      timeout = setTimeout(function(){
        timeout = null;
      }, wait)
      if (callNow) result = func.apply(context, args)
    } else {
      timeout = setTimeout(function(){
        func.apply(context, args)
      }, wait);
    }
    return result;
  };

  debounced.cancel = function() {
    clearTimeout(timeout);
    timeout = null;
  };

  return debounced;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

使用:

var setUseAction = debounce(getUserAction, 10000, true);
// 使用防抖
node.onmousemove = setUseAction

// 取消防抖
setUseAction.cancel()
1
2
3
4
5
6

参考:JavaScript 专题之跟着 underscore 学防抖 (opens new window)

# 函数节流(throttle)

节流的概念可以想象一下水坝,你建了水坝在河道中,不能让水流动不了,你只能让水流慢些。

  • debounce:用户的方法都不执行,n 秒后执行一次;
  • throttle:触发太多了,筛一下,n 秒执行一次;

节流会用在比 input, keyup 更频繁触发的事件中,如 resize, touchmove, mousemove, scroll等。

简单版:使用时间戳来实现,立即执行一次,然后每 N 秒执行一次。

function throttle(func, wait) {
  const context, args
  const previous = 0

  return function() {
    const now = +new Date()
    context = this
    args = arguments
    if (now - previous > wait) {
      func.apply(context, args)
      previous = now
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

最终版:支持取消节流;另外通过传入第三个参数,options.leading 来表示是否可以立即执行一次,options.trailing 表示结束调用的时候是否还要执行一次,默认都是 true。 注意设置的时候不能同时将 leading 或 trailing 设置为 false。

function throttle(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function() {
        previous = options.leading === false ? 0 : new Date().getTime();
        timeout = null;
        func.apply(context, args);
        if (!timeout) context = args = null;
    };

    var throttled = function() {
        var now = new Date().getTime();
        if (!previous && options.leading === false) previous = now;
        var remaining = wait - (now - previous);
        context = this;
        args = arguments;
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout);
                timeout = null;
            }
            previous = now;
            func.apply(context, args);
            if (!timeout) context = args = null;
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining);
        }
    };

    throttled.cancel = function() {
        clearTimeout(timeout);
        previous = 0;
        timeout = null;
    }
    return throttled;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

节流的使用就不拿代码举例了,参考防抖的写就行。

参考:JavaScript 专题之跟着 underscore 学节流 (opens new window)

# 函数柯里化

什么叫函数柯里化?其实就是将使用多个参数的函数转换成一系列使用一个参数的函数的技术。还不懂?来举个例子。

function add(a, b, c) {
  return a + b + c
}
add(1, 2, 3)

let addCurry = curry(add)
addCurry(1)(2)(3)
1
2
3
4
5
6
7

现在就是要实现 curry 这个函数,使函数从一次调用传入多个参数变成多次调用每次传一个参数。

function curry(fn) {
  const judge = (...args) => {
    if (args.length == fn.length) {
      return fn(...args)
    }
    return (...arg) => judge(...args, ...arg)
  }
  return judge
}
1
2
3
4
5
6
7
8
9

# 偏函数

什么是偏函数?偏函数就是将一个 n 参的函数转换成固定 x 参的函数,剩余参数(n - x)将在下次调用全部传入。举个例子:

function add(a, b, c) {
    return a + b + c
}
let partialAdd = partial(add, 1)
partialAdd(2, 3)
1
2
3
4
5

发现没有,其实偏函数和函数柯里化有点像,所以根据函数柯里化的实现,能够能很快写出偏函数的实现:

function partial(fn, ...args) {
    return (...arg) => {
        return fn(...args, ...arg)
    }
}
1
2
3
4
5

如上这个功能比较简单,现在我们希望偏函数能和柯里化一样能实现占位功能,比如:

function clg(a, b, c) {
    console.log(a, b, c)
}
let partialClg = partial(clg, '_', 2)
partialClg(1, 3)  // 依次打印:1, 2, 3
1
2
3
4
5

_ 占的位其实就是 1 的位置。相当于:partial(clg, 1, 2),然后 partialClg(3)。明白了原理,我们就来写实现:

function partial(fn, ...args) {
    return (...arg) => {
        args[index] = 
        return fn(...args, ...arg)
    }
}
1
2
3
4
5
6

# 参考

上次更新: 4/17/2022, 10:59:22 AM