# 实现一个 new 操作符

  • 创建一个新的空对象
  • 使空对象的proto指向构造函数的原型(prototype)
  • 把 this 绑定到空对象
  • 执行构造函数,为空对象添加属性
  • 判断函数的返回值是否为对象,如果是对象,就使用构造函数的返回值,否则返回创建的对象
  • --如果函数没有返回对象类型 Object(包含 Functoin, Array, Date, RegExg, Error),那么 new 表达式中的函数调用将返回该对象引用。
function myNew(Con, ...args) {
  let obj = {};
  obj.__proto__ = Con.prototype;
  let result = Con.call(obj, ...args);
  return result instanceof Object ? result : obj;
}
let lin = myNew(Star, "lin", 18);
// 相当于
let lin = new Star("lin", 18);
1
2
3
4
5
6
7
8
9

# 实现 call

call 核心:

  • 第一个参数为 null 或者 undefined 时,默认上下文为全局对象 window
  • 接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数(将函数设为对象的属性)
  • 为了避免函数名与上下文(context)的属性发生冲突,使用 Symbol 类型
  • 调用函数
  • 函数执行完成后删除 context.fn 属性
  • 返回执行结果
Function.prototype.myCall = function(context = window, ...args) {
  let fn = Symbol("fn");
  context.fn = this; // 这里的this就是需要调用的函数,例子中的 bar(name, age) {}
  // 调用函数
  let result = context.fn(...args); // 这里用了 扩展运算符,将 数组 转换成 序列

  delete context.fn;
  return result;
};

// 测试
let foo = {
  value: 1,
};
function bar(name, age) {
  console.log(name);
  console.log(age);
  console.log(this.value);
}
bar.myCall(foo, "black", "18"); // black 18 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 实现 apply

  • 前部分与 call 一样
  • 第二个参数可以不传,但类型必须为数组或者类数组
Function.prototype.myApply = function(context = window, args) {
  let fn = Symbol("fn");
  context.fn = this;
  let result = context.fn(...args);

  delete context.fn;
  return result;
};
1
2
3
4
5
6
7
8
  • 注:代码实现存在缺陷,当第二个参数为类数组时,未作判断(有兴趣可查阅一下如何判断类数组)

# 实现 bind

bind()方法:会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

  • 对于普通函数,绑定 this 指向
  • 对于构造函数,要保证原函数的原型对象上的属性不能丢失
Function.prototype.myBind = function(context, ...args) {
  const self = this;
  let bindFn = function() {
    self.apply(
      // 对于普通函数,绑定this指向
      // 当返回的绑定函数作为构造函数被new调用,绑定的上下文指向实例对象
      this instanceof bindFn ? this : context,
      args.concat(...arguments)
    ); // ...arguments这里是将类数组转换为数组
  };
  bindFn.prototype = Object.create(self.prototype);
  // 返回一个函数
  return bindFn;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

以下是对实现的分析:

let foo = {
  value: 1,
};
function bar(name, age) {
  console.log(name);
  console.log(age);
  console.log(this.value);
}
bar.myBind(foo, "black", "18");
1
2
3
4
5
6
7
8
9
  • 前几步和之前的实现差不多,就不赘述了
  • bind 返回了一个函数,
  • 对于函数来说有两种方式调用,一种是直接调用,一种是通过 new 的方式,
  • 我们先来说直接调用的方式
  • 对于直接调用来说,这里选择了 apply 的方式实现,
  • 但是对于参数需要注意以下情况:
  • 因为 bind 可以实现类似这样的代码 f.bind(obj, 1)(2),
  • 所以我们需要将两边的参数拼接起来,于是就有了这样的实现 args.concat(...arguments)

# 补充

JavaScript 中 call()、apply()、bind() 的用法 (opens new window)

call、apply、bind 的相同点和不同点:

相同点:

  • 这三个方法都是用来改变函数内 this 的指向。
  • 都可以接收参数。
  • 第一参数都是 this 要指向的对象。

不同点:

  • 传参的方式不同 _ call、bind 方法第二个参数是函数执行时要传入的参数,只能一个一个的传入 _ aplly 方法第二个参数是函数执行时要传入的参数,必须是一个数组
  • 执行方式不同 _ call 方法和 apply 方法都是调用就直接执行; _ bind 是返回一个函数,而不是像 call,apply 一样立即调用,而是需要手动去调用触发函数 bind 传入的参数并不等同于原函数传入的参数,而是在原函数的参数前面作为新增参数,添加进去,原来参数多余的就清除掉

MDN-bind (opens new window)

# 实现instanceof

object instanceof constructor

  • object 某个实例对象, constructor 某个构造函数
  • instanceof 运算符用来检测 某个构造函数.prototype 是否存在于参数 某个实例对象 的原型链上。 instanceof (opens new window)
function myInstanceof(left, right) {
  // 基本数据类型直接返回 false
  if (typeof left !== "object" || left == null) return false;
  // getPrototypeOf 是 Object 对象自带的一个方法,能够拿到参数的原型对象
  let proto = Object.getPrototypeOf(left);
  while (true) {
    // 如果查找到尽头,还没找到,return false
    if (proto == null) return false;
    // 找到相同的原型对象
    if (proto === right.prototype) return true;
    proto = Object.getPrototypeOf(proto);
  }
}

console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String)); //true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 浅拷贝

因为浅拷贝只会将对象的各个属性进行依次复制,并不会进行递归复制。在 JavaScript 中,对于 Object 和 Array 这类引用类型值,当从一个变量向另一个变量复制引用类型值时,这个值的副本其实是一个指针,两个变量指向同一个堆对象,改变其中一个变量,另一个也会受到影响。所以浅拷贝会导致 obj.arr 和 shallowObj.arr 指向同一块内存地址,当修改 obj.arr 的值时,shallowObj.arr 的值同样会被修改 JSON.parse(JSON.stringfy())进行深拷贝方法小结 (opens new window)

1. 循环遍历

let obj2 = {};
// 循环遍历
for (let k in obj1) {
  // k 是属性名  obj1[k] 属性值
  obj2[k] = obj1[k];
}
1
2
3
4
5
6

2. ES6 语法 Object.assign

let obj2 = {};
Object.assign(obj2, obj1);

//补充
Object.assign(); // 方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。
Object.assign(target, ...sources);
1
2
3
4
5
6

3. ES6 语法 扩展运算符

let obj2 = { ...obj1 };
1

4. Array.from()

// 语法:
let arr1 = [1, 2, 3, 4];
let arr2 = Array.from(arr1);
1
2
3

5. arr.slice()

// 语法: arr.slice(begin, end)
let arr1 = [1, 2, 3, 4];
let arr2 = arr1.slice();
1
2
3

补充:说明了赋值、浅拷贝、深拷贝的区别三元同学 (opens new window)

# 深拷贝

而深拷贝则不同,它不仅将原对象的各个属性逐个复制出去,而且将原对象各个属性所包含的对象也依次采用深拷贝的方法递归复制到新对象上。这就不会存在上面 obj 和 shallowObj 的 arr 属性指向同一个对象的问题。当修改 obj.arr 的值时,shallowObj.arr 的值不会被修改,仍然为原值 JSON.parse(JSON.stringfy())进行深拷贝方法小结 (opens new window)

1. 简易版及问题

JSON.parse(JSON.stringify(obj));
1

JOSN 对象中的 stringify 可以把一个 JSON 对象序列化为一个 JSON 字符串,parse 可以把 JSON 字符串反序列化为一个 JSON 对象,通过这两个方法,也可以实现对象的深复制。

该方法的局限性:

  • 无法解决循环引用的问题。举个例子: const a = {val:2};a.target = a; 拷贝 a 会出现系统栈溢出,因为出现了无限递归的情况。

  • 无法拷贝一些特殊的对象,诸如 RegExp, Date, Set, Map 等

  • 无法拷贝函数

  • 会忽略 undefined/symbol

2. 面试可用版

function deepClone(obj) {
  if (typeof obj === "object" && obj !== null) {
    //初始化返回结果
    let result = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
      // 保证 key 不是原型上的属性
      if (obj.hasOwnProperty(key)) {
        // 递归调用
        result[key] = deepClone(obj[key]);
      }
    }
    return result;
  } else {
    //简单数据类型 直接 赋值
    return obj;
  }
}
// 问题
// ? let key in obj 会遍历到 obj 原型上的属性吗?这个判断条件有没有必要
// for...in语句以任意顺序遍历一个对象的除Symbol以外的可枚举属性,包括继承的可枚举属性。
// 对象继承的属性属于自己的属性,hasOwnProperty(key) 为 true

// 考虑循环引用
// 我们执行下面这样一个测试用例:
const target = {
    field1: 1,
    field2: undefined,
    field3: {
        child: 'child'
    },
    field4: [2, 4, 8]
};
target.target = target;

function clone(target, map = new Map()) {
  if (typeof target === "object") {
    let cloneTarget = Array.isArray(target) ? [] : {};
    // if (map.has(target))
    if (map.get(target)) {
      return map.get(target);
    }
    map.set(target, cloneTarget);
    for (const key in target) {
      cloneTarget[key] = clone(target[key], 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

补充

# 数组去重

JavaScript 数组去重(12 种方法,史上最全) (opens new window) 总结下来楼主这 12 中无非就两类:

  1. 两层循环法
  2. 利用语法自身键不可重复性 其他的都是用了语言不通的 api,算不上单独的一种方法,比如 indexOf 和 includes 两层循环 都是同一类

Array.prototype.flat() (opens new window)

function dedupe(array) {
  // return Array.from(new Set(array));
  return [...new Set(array)];
}

dedupe([1, 1, 2, 3]); // [1, 2, 3]
1
2
3
4
5
6
  • 编写一个数组去重函数,对数组进行去重处理
  • 输入:由数字、字符串、数组、对象等类型元素组成的数组
  • 输出:去重后的数组

解法一:

function unique(arr) {
  // 编写代码
  let b = [],
    hash = {};
  for (let i = 0; i < arr.length; i++) {
    if (!hash[JSON.stringify(arr[i])]) {
      hash[JSON.stringify(arr[i])] = true;
      b.push(arr[i]);
    }
  }
  return b;
}
// 测试
let arr = [{ d: 2 }, { e: 3 }, 2, 2, { d: 2 }];
console.log(unique(arr)); // [{'d': 2}, {'e': 3}, 2]
console.log(dedupe(arr)); // [{'d': 2}, {'e': 3}, 2, {'d': 2}]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

解法二:

function unique(arr) {
  // 编写代码
  // JSON.stringify(item) 是把数组中的对象解析成字符串再进行比较
  let b = arr.map((item) => {
    return JSON.stringify(item);
  });
  // Set(b) 用来对 b 去重,但是去重后的结果不是我们想要的数组的形式
  let c = Array.from(new Set(b));
  let d = c.map((item) => {
    return JSON.parse(item);
  });
  return d;
}
// 2
const unique = (arr) => {
  return [...new Set(arr.map((item) => JSON.stringify(item)))].map((item) =>
    JSON.parse(item)
  );
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 数组扁平化

# 防抖

# 参考

# lin

所谓防抖,就是指触发事件后在设定的时间期限内函数只能执行一次,如果在设定的时间期限内又触发了事件,则会重新计算函数执行时间。

function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    let context = this;
    if (timer) clearTimeout(timer);
    timer = setTimeout(function() {
      fn.apply(context, args);
      timer = null;
    }, delay);
  };
}
1
2
3
4
5
6
7
8
9
10
11
function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this, args);
      timer = null;
    }, delay);
  };
}
1
2
3
4
5
6
7
8
9
10

完整版

function debounce(func, wait, immediate) {
  let timer, result;
  const debounced = function(...args) {
    if (timer) clearTimeout(timer);
    if (immediate) {
      let callNow = !timer;
      timer = setTimeout(() => {
        timer = null;
      }, wait);
      if (callNow) result = func.apply(this, args);
    } else {
      timer = setTimeout(() => {
        result = func.apply(this, args);
        timer = null;
      }, wait);
    }
    return result;
  };
  debounced.cancel = () => {
    clearTimeout(timer);
    timer = 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

# 节流

# 节流-时间戳

function throttle(fn, delay = 100) {
  let previous = 0;
  return function(...args) {
    let now = +new Date();
    if (now - previous > delay) {
      previous = now;
      fn.apply(this, args);
    }
  };
}
1
2
3
4
5
6
7
8
9
10

# 补充

new Date(); // Mon Nov 29 2021 14:40:54 GMT+0800 (中国标准时间)
+new Date(); // 1638168037579
new Date().valueOf(); // 1638167980665
new Date().getTime(); // 1638167989231
1
2
3
4

# 节流-定时器

所谓节流,就是指连续触发事件但是在设定时间内中只执行一次函数。

function throttle(fn, delay = 100) {
  let timer = null;
  return function(...args) {
    if (!timer) {
      timer = setTimeout(() => {
        fn.apply(this, args);
        timer = null;
      }, delay);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11

# 比较

比较两个方法:

  • 第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行
  • 第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件

# 节流-双剑合璧

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

function throttle3(fn, delay) {
  let timer,
    previous = 0;
  const throttled = function(...args) {
    let now = +new Date();
    //下次触发 fn 剩余的时间
    let remaining = delay - (now - previous);
    // 如果没有剩余的时间了或者你改了系统时间
    // delay <= now - previous,  now < previous
    // if (remaining <= 0 || remaining > delay) {
    if (delay <= now - previous || now < previous) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      previous = now;
      fn.apply(this, args);
    } else if (!timer) {
      timer = setTimeout(() => {
        previous = +new Date();
        timer = null;
        fn.apply(this, args);
      }, remaining);
    }
  };
  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

优化:但是我有时也希望无头有尾,或者有头无尾,这个咋办?

  • 那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定:
    • leading:false 表示禁用第一次执行
    • trailing: false 表示禁用停止触发的回调
function throttle3(fn, delay, options) {
  let timer,
    previous = 0;
  const throttled = function(...args) {
    let now = +new Date();
    if (!previous && options.leading === false) previous = now;
    let remaining = delay - (now - previous);
    if (delay <= now - previous || now < previous) {
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      previous = now;
      fn.apply(this, args);
      // ? if (!timeout) context = args = null;
    } else if (!timer && options.trailing !== false) {
      timer = setTimeout(() => {
        previous = options.leading === false ? 0 : new Date.getTime();
        timer = null;
        fn.apply(this, args);
        // ? if (!timeout) context = args = null;
      }, remaining);
    }
  };
  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

# 懒加载

# 简易版

// 获取所有的图片标签
const imgs = document.getElementsByTagName("img");
// 获取可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
// num 用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
let num = 0,
  len = imgs.length;

function lazyload() {
  for (let i = num; i < len; i++) {
    // 用可视区域高度 减去 元素顶部距离可视区域顶部的高度
    let distance = viewHeight - imgs[i].getBoundingClientRect().top;
    // 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
    // 在chrome浏览器中 正常
    if (distance >= 50) {
      // 给元素写入真实的 src,展示图片
      imgs[i].src = imgs[i].getAttribute("data-src");
      // 前i张图片已经加载完毕,下次从第i+1张开始检查是否露出
      num = i + 1;
    }
  }
}

// 监听scroll事件
// window.addEventListener('scroll', lazyload)

// 当然,最好对 scroll 事件做节流处理,以免频繁触发:
window.addEventListener("scroll", throttle(lazyload, 200));
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

# 增强版

  • 图片全部加载完成后移除事件监听
  • 加载完的图片,从 imgList 移除
// 获取所有的图片标签
const imgs = document.getElementsByTagName("img");
// 获取可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
// num 用于统计当前显示到了哪一张图片,避免每次都从第一张图片开始检查是否露出
let num = 0,
  len = imgs.length;

function lazyload() {
  let deleteIndexList = [];
  for (let i = num; i < len; i++) {
    // 用可视区域高度 减去 元素顶部距离可视区域顶部的高度
    let distance = viewHeight - imgs[i].getBoundingClientRect().top;
    // 如果可视区域高度大于等于元素顶部距离可视区域顶部的高度,说明元素露出
    // 在chrome浏览器中 正常
    if (distance >= 50) {
      // 给元素写入真实的 src,展示图片
      imgs[i].src = imgs[i].getAttribute("data-src");
      deleteIndexList.push(i);
      // 前i张图片已经加载完毕,下次从第i+1张开始检查是否露出
      num = i + 1;
      if (num === len) {
        document.removeEventListener("scroll", lazyload);
      }
    }
  }
  imgs = imgs.filter((_, index) => !deleteIndexList.includes(index));
}
// 监听scroll事件,最好对 scroll 事件做节流处理,以免频繁触发:
window.addEventListener("scroll", throttle(lazyload, 200));
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

# others

图片懒加载 (opens new window)

// 获取所有的图片标签
const imgs = document.getElementsByTagName("img");
// 获取可视区域的高度
const viewHeight = window.innerHeight || document.documentElement.clientHeight;

const lazyload = (function() {
  let num = 0,
    len = imgs.length;
  return function() {
    let deleteIndexList = [];
    for (let i = num; i < len; i++) {
      let distance = viewHeight - imgs[i].getBoundingClientRect().top;
      if (distance >= 50) {
        imgs[i].src = imgs[i].getAttribute("data-src");
        deleteIndexList.push(i);
        num = i + 1;
        if (num === len) {
          document.removeEventListener("scroll", lazyload);
        }
      }
    }
    imgs = imgs.filter((_, index) => !deleteIndexList.includes(index));
  };
})();
// 监听scroll事件, scroll 事件做节流处理,以免频繁触发:
window.addEventListener("scroll", throttle(lazyload, 200));
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 Publisher {
  constructor() {
    this.observers = [];
    console.log("Publisher created");
  }
  add(observer) {
    console.log("Publisher.add invoked");
    this.observers.push(observer);
  }
  remove(observer) {
    console.log("Publisher.remove invoked");
    this.observers.forEach((item, i) => {
      if (item === observer) {
        this.observers.splice(i, 1);
      }
    });
  }
  notify() {
    this.observers.forEach((observer) => {
      observer.update(this);
    });
  }
}

class Observer {
  constructor() {
    console.log("Observer created");
  }
  update() {
    console.log("Observer.update invoked");
  }
}
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

补充学习:

# 补充

笔试编程题,自己写输入输出

# 参考

「中高级前端面试」JavaScript 手写代码无敌秘籍 (opens new window)

初、中级前端应该要掌握的手写代码实现 (opens new window)

原生 JS 灵魂之问(中) (opens new window)

上次更新: 2021年12月30日星期四晚上8点58分