# 手写一个 new 操作符

答案解析

  • new 操作符做了以下这些事:

(1)它创建了一个全新的对象。

(2)它会将新对象的 proto 属性链接到这个函数的 prototype 对象上。

(3)它会使 this 指向新创建的对象。

(4)如果函数没有返回对象类型 Object(包含 Function,Array,Date,RegExg,Error),那么 new 表达式中的函数调用将返回该对象引用。

// 版本一
function myNew(func) {
  return function() {
    // 创建一个新对象并将其隐式原型指向构造函数原型
    let obj = {
      __proto__: func.prototype
    }
    // 执行构造函数
    func.call(obj, ...arguments)
    // 返回该对象
    return obj
  }
}

function person(name, age) {
  this.name = name
  this.age = age
}
let obj = myNew(person)('lin', 18)
console.log(obj) // person {name: "lin", age: 18}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 版本二
function myNew(func) {
  let res = {}
  if (func.prototype !== null) {
    res.__proto__ = func.prototype
  }
  let ret = func.apply(res, Array.prototype.slice.call(arguments, 1))
  if ((typeof ret === 'object' || typeof ret === 'function') && ret !== null) {
    return ret
  }
  return res
}

function person(name, age) {
  this.name = name
  this.age = age
}
let obj = myNew(person, 'lin', 18)
console.log(obj) // person {name: "lin", age: 18}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 手写一个 JSON.stringfy 和 JSON.parse

答案解析

JSON (opens new window) 对象包含两个方法: 用于解析 JavaScript Object Notation (JSON) 的 parse() 方法,以及将对象/值转换为 JSON字符串的 stringify() 方法。

  • 实现 JSON.stringfy 需要遵循以下原则:

(1)转换值如果含有 toJSON() (opens new window) 方法,直接调用该方法转换,否则调用 toString()。

(2)非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。

(3)布尔值(Boolean)、数字(Number)、字符串(String)的包装对象在序列化过程中会自动转换成对应的原始值。

(4)undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略(出现在非数组对象的属性值中时),或者被转换成 null(出现在数组中时)。

(5)函数、undefined 被单独转换时,会返回 undefined,如 JSON.stringify(function() {}) 和 JSON.stringify(undefined) 都会返回 undefined。

(6)不可枚举的属性会被忽略。

(7)如果一个对象的属性值通过某种间接的方式指回该对象本身,即循环引用,属性也会被忽略。

function jsonStringify(jsonObj) {
    let result = '', curVal;
    if (jsonObj === null) {
        return String(jsonObj);
    }
    switch (typeof jsonObj) {
        case 'number':
        case 'boolean':
            return String(jsonObj);
        case 'string':
            return '"' + jsonObj + '"';
        case 'undefined':
        case 'function':
            return undefined;
    }

    switch (Object.prototype.toString.call(jsonObj)) {
        case '[object Array]':
            result += '[';
            for (let i = 0, len = jsonObj.length; i < len; i++) {
                curVal = JSON.stringify(jsonObj[i]);
                result += (curVal === undefined ? null : curVal) + ",";
            }
            if (result !== '[') {
                result = result.slice(0, -1);
            }
            result += ']';
            return result;
        case '[object Date]':
            return '"' + (jsonObj.toJSON ? jsonObj.toJSON() : jsonObj.toString()) + '"';
        case '[object RegExp]':
            return "{}";
        case '[object Object]':
            result += '{';
            for (let i in jsonObj) {
                if (jsonObj.hasOwnProperty(i)) {
                    curVal = JSON.stringify(jsonObj[i]);
                    if (curVal !== undefined) {
                        result += '"' + i + '":' + curVal + ',';
                    }
                }
            }
            if (result !== '{') {
                result = result.slice(0, -1);
            }
            result += '}';
            return result;

        case '[object String]':
            return '"' + jsonObj.toString() + '"';
        case '[object Number]':
        case '[object Boolean]':
            return jsonObj.toString();
    }
}
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
48
49
50
51
52
53
54
55
function jsonParse(jsonObj) {
  return eval('(' + jsonObj + ')');
}
1
2
3

# 手写一个 call 或 apply

答案解析

Function.prototype.myCall = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('not function');
  }
  context = context || window;
  context.fn = this;
  let args = [...arguments].slice(1);
  let result = context.fn(...args);
  delete context.fn;
  return result;
}
1
2
3
4
5
6
7
8
9
10
11
Function.prototype.myApply = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('not function');
  }
  context = context || window;
  context.fn = this;
  let result;
  if (arguments[1]) {
    result = context.fn(...arguments[1])
  } else {
    result = context.fn();
  }
  delete context.fn;
  return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 手写一个 Function.prototype.bind

答案解析

// 简化版
Function.prototype.simpleBind = function(context) {
  var self = this; // 保存原函数
  return function() { // 返回一个新的函数
    return self.apply(context, arguments); // 执行新函数时,会把之前传入的 context 当作新函数体内的 this
  }
}

// 复杂版,可以预先填入一些参数
Function.prototype.complexBind = function() {
  var self = this, // 保存原函数
    context = [].shift.call(arguments), // 需要绑定的 this 上下文
    args = [].slice.call(arguments); // 剩余的参数转成数组
  return function() {
    // 执行新函数时,会把之前传入的 context 当作新函数体内的 this,并且组合两次分别传入的参数,作为新函数的参数
    return self.apply(context, [].concat.call(args, [].slice.call(arguments)));
  }
}

// 参考 MDN 上的 bind 实现
Function.prototype.mdnBind = function(context) {
  if (typeof this !== 'function') {
    throw new TypeError('not function');
  }
  const _this = this;
  const argus = [...arguments].slice(1);
  return function F() {
    //因为返回了一个函数,可以 new F(),所以需要判断
    if (this instanceof F) {
      return new _this(...argus,...arguments);
    }
    return _this.apply(context, argus.concat(...arguments));
  }
}
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

# 手写防抖(Debouncing)和节流(Throttling)

  • 所谓防抖,是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时。

  • 防抖是将多次执行变成一次执行。

  • 防抖的应用场景:搜索联想词、点击按钮提交表单等等。

// 非立即执行版
function debounce(fn, wait) {
  let timer = null;
  return function() {
    let context = this;
    let args = arguments;
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(context, args);
    }, wait);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
// 立即执行版
function debounce(fn, wait) {
  let timer = null;
  return function() {
    let context = this;
    let args = arguments;
    if (timer) clearTimeout(timer);
    let callNow = !timer;
    timer = setTimeout(() => {
      timer = null;
    }, wait);
    if (callNow) fn.apply(context, args);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 所谓节流,是指在规定时间内,只能触发一次函数。如果在规定时间内触发多次函数,只有一次生效。

  • 节流是将多次执行变为每隔一段时间执行一次。

  • 应用场景:监听滚动事件,比如滚动到底部自动加载,鼠标移动等等。

// 时间戳版
function throttle(fn, wait) {
  let last = 0;
  return function() {
    let context = this;
    let args = arguments;
    let now = Date.now();
    if (now - last >= wait) {
      fn.apply(context, args);
      last = now;
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 定时器版
function throttle(fn, wait) {
  let timer = null;
  return function() {
    let context = this;
    let args = arguments;
    if (!timer) {
      timer = setTimeout(() => {
        timer = null;
        fn.apply(context, args);
      }, wait);
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 手写一个 JS 深拷贝(由浅入深多种写法)

答案解析

在你不知道的 JavaScript(上)中的对象部分已有整理过。

# 手写一个 instanceOf

答案解析

  1. instanceof (opens new window) 主要用于判断某个实例是否属于某个类型,也可用于判断某个实例是否是其父类型或者祖先类型的实例。

  2. instanceof 的实现原理是只要右边变量的 prototype 在左边变量的原型链上即可。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false。

function myInstanceOf(instance, constructor) {
  let proto = instance.__proto__;
  let prototype = constructor.prototype;
  while (proto) {
    if (proto === prototype) return true;
    proto = proto.__proto__;
  }
  return false;
}

// 测试
function C(){} 
function D(){}
let c = new C();
let d = new D();
console.log(myInstanceOf(c, C)) // true
console.log(myInstanceOf(c, D)) // false
console.log(myInstanceOf(d, D)) // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 手写一个 map、reduce 和 filter

答案解析

  • map() (opens new window) 方法创建一个新数组,其结果是该数组中的每个元素是调用一次提供的函数后的返回值。
function myMap(arr, callback) {
  if (!Array.isArray(arr) || !arr.length || typeof callback !== 'function') {
    return [];
  } else {
    let newArr = [];
    for (let i = 0; i < arr.length; i++) {
      newArr[i] = callback(arr[i], i, arr);
    }
    return newArr;
  }
}

// 测试
let arr = [1,2,3,4,5];
console.log(myMap(arr, item => item * 3));  // [3, 6, 9, 12, 15]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • reduce() (opens new window) 方法对数组中的每个元素执行一个由您提供的 reducer 函数(升序执行),将其结果汇总为单个返回值。
function myReduce(arr, callback, init) {
  if (!Array.isArray(arr) || !arr.length || typeof callback !== 'function') {
    return [];
  } else {
    let hasInitValue = init === 0 ? (!init) : (!!init);
    let res = hasInitValue ? init : arr[0];
    for (let i = hasInitValue ? 0 : 1; i < arr.length; i++) {
      res = callback(res, arr[i], i, arr)
    }
    return res
  }
}

// 测试
let arr = [1,2,3,4,5];
console.log(myReduce(arr, (a, b) => a + b));     // 15
console.log(myReduce(arr, (a, b) => a + b, 5));  // 20
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function myFilter(arr, callback) {
  if (!Array.isArray(arr) || !arr.length || typeof callback !== 'function') {
    return [];
  } else {
    let newArr = [];
    for (let i = 0; i < arr.length; i++) {
      if (callback(arr[i], i, arr)) {
        newArr.push(arr[i]);
      }
    }
    return newArr;
  }
}

// 测试
let arr = [1,2,3,4,5];
console.log(myFilter(arr, item => item > 2));  // [3,4,5]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 手写实现拖拽

答案解析

<div id="box" style="position: absolute; width: 100px; height: 100px; background: red; cursor: move;"></div>
1
window.onload = function() {
  let drag = document.getElementById('box');
  drag.onmousedown = function(e) {
    var e = e || window.event;
    // 鼠标与拖拽元素边界的距离 = 鼠标与可视区域边界的距离 - 拖拽元素与可视区域边界的距离
    let diffX = e.clientX - drag.offsetLeft;
    let diffY = e.clientY - drag.offsetTop;
    drag.onmousemove = function(e) {
      // 拖拽元素移动的距离 = 鼠标与可视区域边界的距离 - 鼠标与拖拽元素边界的距离
      let left = e.clientX - diffX;
      let top = e.clientY - diffY;
      // 避免拖出可视区域
      if (left < 0) {
        left = 0;
      } else if (left > window.innerWidth - drag.offsetWidth) {
        left = window.innerWidth - drag.offsetWidth;
      }
      if (top < 0) {
        top = 0;
      } else if (top > window.innerHeight - drag.offsetHeight) {
        top = window.innerHeight - drag.offsetHeight;
      }
      drag.style.left = left + 'px';
      drag.style.top = top + 'px';
    };
    drag.onmouseup = function(e) {
      this.onmousemove = null;
      this.onmouseup = null;
    };
  };
};
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

# 使用 setTimeout 模拟 setInterval

答案解析

arguments.callee (opens new window) 属性包含当前正在执行的函数。

setTimeout(function() {
  // do some here
  setTimeout(arguments.callee, 1000);
}, 1000)
1
2
3
4

# 手写实现 Object.create 的基本原理

答案解析

Object.create() (opens new window) 方法创建一个新对象,使用现有的对象来提供新创建的对象的 __proto__

// 将传入的对象作为原型
function myObjectCreate(obj) {
  function F() {};
  F.prototype = obj;
  return new F();
}
1
2
3
4
5
6
上次更新时间: 2023年12月27日 21:57:42