手写常用方法

数据类型判断

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

typeof undefined  // 'undefined'
typeof Symbol.for("a")  // 'symbol'
typeof (()=> 1)  // 'function'
typeof null  // 'object'
typeof {}  // 'object'

Object.prototype.toString.call(1)  // '[object Number]'
1
2
3
4
5
6
7

🚀 实现一个类型判断方法:

function typeOf(param) {
  return Object.prototype.toString.call(param).slice(8, -1);
}

typeOf("")  // 'String'
typeOf(Symbol())  // 'Symbol'
typeOf(()=> 1)  // 'Function'
typeOf({})  // 'Object'
typeOf(null)  // 'Null'
typeOf(undefined)  // 'Undefined'
typeOf(new Date())  // 'Date'
1
2
3
4
5
6
7
8
9
10
11

继承

🌐 原型和原型链 (opens new window)在新窗口打开

原型链继承

通过构造函数的prototype指向原型对象

function Person() {
  this.colors = ['yellow', 'white']
}
Person.prototype.getColors = function() {
  return this.color;
}

function Doctor() {};
Doctor.prototype = new Person();

// ----------Testing----------
const doc = new Doctor();
doc.colors.push('blue');
const doc1 = new Doctor();

console.log(doc1.colors);  // ['yellow', 'white', 'black']
doc instanceof(Doctor);  // true
doc instanceof(Person);  // true
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

原型链继承:

  • 🚨 原型中包含的引用类型属性将被所有实例共享;
  • 🚨 子类在实例化的时候不能给父类构造函数传参;

借用构造函数实现继承

function Person(name) {
    this.name = name;
    this.getName = function() {
        return this.name;
    }
}
function Lawyer(name) {
    // 为Lawyer实例对象添加Person的属性
    Person.call(this, name);
}
Lawyer.prototype = new Person();

// ----------Testing----------
const ll = new Lawyer("Michael")
1
2
3
4
5
6
7
8
9
10
11
12
13
14

借用构造函数实现继承:

  • 🎉 由于每次实例化子类都重新创建了一次属性,所以不再存在引用类型共享的问题

  • 🎉 通过call方法调用父类构造器,允许在实例化的时候往父类传参

  • 🚨 子类实例每次都要创建一次方法,内存开销大

组合继承

🚀 组合继承结合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

function Animal(name) {
    this.name = name;
    this.colors = ['black', 'white'];
}
Animal.prototype.getName = function() {
    return this.name;
}
function Dog(name, age) {
    // 子类实例上声明父类实例的属性
    Animal.call(this, name);
    this.age = age;
}
// 原型继承,获得父类原型对象方法的访问能力
Dog.prototype = new Animal();
Dog.prototype.constructor = Dog;

// ----------Testing----------
let dog1 = new Dog('奶昔', 2);
dog1.colors.push('brown');
// { name: "哈赤", colors: ["black", "white", "brown"], age: 1 }

let dog2 = new Dog('哈赤', 1);
// { name: "哈赤", colors: ["black", "white"], age: 1 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

寄生式组合继承 ✅

🔥 组合继承已经相对完善了,但还是存在问题,它的问题就是调用了 2 次父类构造函数,第一次是在 new Animal(),第二次是在 Animal.call() 这里。

🚀 所以解决方案就是不直接调用父类构造函数给子类原型赋值,而是通过创建空函数 F 获取父类原型的副本,区别如下:

- Dog.prototype =  new Animal()
- Dog.prototype.constructor = Dog

+ function F() {}
+ F.prototype = Animal.prototype
+ let f = new F()
+ f.constructor = Dog
+ Dog.prototype = f
1
2
3
4
5
6
7
8

稍微封装下上面添加的代码后:

function object(o) {
    function F() {}
    F.prototype = o
    return new F()
}
function inheritPrototype(child, parent) {
    let prototype = object(parent.prototype)
    prototype.constructor = child
    child.prototype = prototype
}
inheritPrototype(Dog, Animal)
1
2
3
4
5
6
7
8
9
10
11

🎉 其实这就是Object.create方法的实现,可以基于组合继承的代码改成最简单的寄生式组合继承:

- Dog.prototype =  new Animal()
- Dog.prototype.constructor = Dog

+ Dog.prototype =  Object.create(Animal.prototype)
+ Dog.prototype.constructor = Dog
1
2
3
4
5

最终代码

function Animal(name) {
    this.name = name;
    this.colors = ['black', 'white'];
}
Animal.prototype.getName = function() {
    return this.name;
}
function Dog(name, age) {
    // 子类实例上声明父类实例的属性
    Animal.call(this, name);
    this.age = age;
}
// 原型继承,获得父类原型对象方法的访问能力
Dog.prototype = Object.create(Animal.prototype)Dog.prototype.constructor = Dog;

let dog1 = new Dog('奶昔', 2);
dog1.colors.push('brown');
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

class 实现继承

🌐 ES6 class的继承原理 (opens new window)在新窗口打开

class Animal {
    constructor(name) {
        this.name = name
    }
    getName() {
        return this.name
    }
}
class Dog extends Animal {
    constructor(name, age) {
        super(name)
        this.age = age
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

函数柯里化

柯里化

  • 柯里化(Currying)是一种关于函数的高阶技术。它不仅被用于 JavaScript,还被用于其他编程语言。
  • 柯里化是一种函数的转换,它是指将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)
  • 柯里化不会调用函数。它只是对函数进行转换。
// 高级柯里化实现
function curry(fn) {
    return function curried(...args) {
        // 闭包内保存着 args
        if (args.length >= fn.length) {
            return fn(...args);
        }
        return function (...subArgs) {
            // args 需要更新
            return curried(...args.concat(subArgs));
        };
    };
}

function plus(a, b, c) {
    return a + b + c;
}

const hello = curry(plus);
console.log(hello(1, 2)(5));  // 8
console.log(hello(1)(2, 5));  // 8
console.log(hello(1)(2)(5));  // 8
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

只允许确定参数长度的函数

柯里化要求函数具有固定数量的参数。

使用 rest 参数的函数,例如 f(...args),不能以这种方式进行柯里化。

比柯里化多一点

根据定义,柯里化应该将 sum(a, b, c) 转换为 sum(a)(b)(c)

但是,如前所述,JavaScript 中大多数的柯里化实现都是高级版的:它们使得函数可以被多参数变体调用。

数组去重

双层for循环

function distinct(arr) {
  for (let i = 0, len = arr.length; i < len; i++) {
    for (let j = i + 1; j < len; j++) {
      if (arr[i] == arr[j]) {
        arr.splice(j, 1)
        // splice 会改变数组长度,所以要将数组长度 len 和下标 j 减一
        len--
        j--
      }
    }
  }
  return arr
}
1
2
3
4
5
6
7
8
9
10
11
12
13

分析:双重 for 循环是比较笨拙的方法,它实现的原理很简单:先定义一个包含原始数组第一个元素的数组,然后遍历原始数组,将原始数组中的每个元素与新数组中的每个元素进行比对,如果不重复则添加到新数组中,最后返回新数组;因为它的时间复杂度是O(n^2),如果数组长度很大,效率会很低

Array.filter() 加 indexOf

ES5 利用indexOf方法返回寻第一个匹配下标的特性

const unique2 = (arr) => arr.filter((item, index, array)=> array.indexOf(item) === index);
1

ES6 中的 Set 去重

🌐 ES6 Set (opens new window)在新窗口打开

const unique1 = (arr) => [...new Set(arr)];
1

Object 键值对

时间复杂度最小,但是空间复杂度较大

function distinct(array) {
  var obj = {}
  return array.filter(function(item, index, array) {
    return obj.hasOwnProperty(typeof item + item)
      ? false
      : (obj[typeof item + item] = true)
  })
}
1
2
3
4
5
6
7
8

这种方法是利用一个空的 Object 对象,我们把数组的值存成 Object 的 key 值,比如 Object[value1] = true,在判断另一个值的时候,如果 Object[value2]存在的话,就说明该值是重复的,但是最后请注意这里obj[typeof item + item] = true没有直接使用obj[item],是因为 因为 123 和 '123' 是不同的,直接使用前面的方法会判断为同一个值,因为对象的键值只能是字符串,所以我们可以使用 typeof item + item 拼成字符串作为 key 值来避免这个问题。

数组扁平化

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

[1, [2, [3]]].flat(2);  // [1, 2, 3] 参数2表示铺平的层级
1

ES5 实现:递归即可

function flat(arr) {
    let result = []
    for(let i = 0; i<arr.length; i++) {
        const item = arr[i]
        if(Array.isArray(item)) {
            result = result.concat(flat(item))
        }else {
            result.push(item)
        }
    }
    return result
}

// ----------Testing----------
flat([1,2,[3,4, [5]]])  // [1, 2, 3, 4, 5]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

ES6 实现

// [].concat(1,2,3,[4,5])   [1, 2, 3, 4, 5]
// concat的参数是以逗号分分隔的任何数据类型,一维数组会直接被解构推入到新数组当中
function flat(arr) {
    while(arr.some(i=> Array.isArray(i))) {
        arr = [].concat(...arr)
    }
    return arr
}
}

// ----------Testing----------
flat([1,2,[3,4, [5]]])  // [1, 2, 3, 4, 5]
1
2
3
4
5
6
7
8
9
10
11
12

深浅拷贝

浅拷贝

只处理第一层数据,对对象类型数据进行拷贝

🌐 hasOwnProperty (opens new window)在新窗口打开

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;
    const newObj = obj instanceof Array ? [] : {};
    for (let 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

事件总线(发布订阅)

观察者模式

一个对象(观察者)订阅另一个对象(主题),当主题被激活的时候,触发观察者里面的事件。(观察者自身维护着事件)

大白话解释:当去你去医院打吊瓶(某些地方叫打点滴,就是这么严谨)的时候,医生要观察吊瓶的改变,当快打完的时候,就要通知医生来取针。这里医生就是观察者(Obeserver),吊瓶就是被观察者/主题。

class Subject {  // 被观察者
  constructor(name){
    this.state = 100;
    this.name = name;
    this.obs = []  // 会有多个观察者
  }
  addObs(ob){
    this.obs.push(ob)
  }
  setState(state){  // 改变状态的方法
    this.state = state
    // 通知观察者执行动作
    this.obs.forEach((ob)=>{
      ob.update(this)  // 让观察者自身去做更新
    })
  }
}

class Obeserver{  // 观察者
  constructor(name){
    this.name = name;
  }
  update(subject){
    // 这里观察者也有可能观察多个
    if (!subject.state) {
      console.log(`${this.name} 收到通知 :${subject.name} 的 带瓶打完啦!`)
    }else {
      console.log(` ${this.name} 收到通知 :${subject.name} 的 带瓶量: ${subject.state}`)
    }

  }

}
var zhiliao = new Subject();
var hushi = new Obeserver("护士");
var yisheng = new Obeserver("医生")

zhiliao.addObs(hushi)  // 把观察者放到被观察这的里面去
zhiliao.addObs(yisheng)
zhiliao.setState(50) 
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

发布订阅

订阅者把自己想要订阅的事件注册到调度中心,当发布者发布事件到调度中心(就是该事件被触发),再由调度中心统一调度订阅者注册到调度中心的处理代码。

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

字符串模板

function render(template, data) {
    const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
    if (reg.test(template)) { // 判断模板里是否有模板字符串
        const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
        template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
        return render(template, data); // 递归的渲染并返回渲染后的结构
    }
    return template; // 如果模板没有模板字符串直接返回
}
1
2
3
4
5
6
7
8
9

测试:

let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let person = {
    name: '布兰',
    age: 12
}
render(template, person); // 我是布兰,年龄12,性别undefined
1
2
3
4
5
6

图片懒加载

与普通的图片懒加载不同,如下这个多做了 2 个精心处理:

  • 图片全部加载完成后移除事件监听;
  • 加载完的图片,从 imgList 移除;
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length

// 修正错误,需要加上自执行
const imgLazyLoad = (function() {
  let count = 0

  return function() {
    let deleteIndexList = []
    imgList.forEach((img, index) => {
      let rect = img.getBoundingClientRect()
      if (rect.top < window.innerHeight) {
        img.src = img.dataset.src
        deleteIndexList.push(index)
        count++
        if (count === length) {
          document.removeEventListener('scroll', imgLazyLoad)
        }
      }
    })
    imgList = imgList.filter((img, index) => !deleteIndexList.includes(index))
  }
})()

// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad)
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

参考:图片懒加载在新窗口打开

函数防抖

防抖,debounce,后执行

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

简单版

函数内部支持使用 this 和传递参数;

function debounce(fn, timeout = 1000) {
  let timer = null;

  return function (...args) {
    const context = this;
    if (timer) clearTimeout(timer);

    timer = setTimeout(fn.bind(context, ...args), timeout);
  };
}
1
2
3
4
5
6
7
8
9
10

使用:

function fn1(...args) {
  console.log(this);
  console.log(args);
}

const obj = {
  name: "hello",
  action: debounce(fn1, 3000),
};

obj.action(1);
obj.action(2);
obj.action(3);
obj.action(4);
// { name: 'hello', action: [Function (anonymous)] }
// [ 4 ]
// [Done] exited with code=0 in 3.056 seconds
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

最终版

除了支持 this 和参数外,还支持以下功能:

  • 支持立即执行;
  • 函数可能有返回值;(只有立即执行才能拿到返回值)
  • 支持取消;
function debounce(fn, timeout, immediate = false) {
  let timer, result;

  const debouncedFn = function (...args) {
    const context = this;
    if (timer) clearTimeout(timer);

    if (immediate) {
      immediate = false;
      result = fn.apply(context, args);
    }

    timer = setTimeout(fn.bind(context, ...args), timeout);
    
    return result;
  };

  debouncedFn.cancel = function () {
    clearTimeout(timer);
    timer = null;
  };

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

使用:

<!DOCTYPE html>
<html lang="zh-cmn-Hans">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="x-ua-compatible" content="IE=edge, chrome=1" />
    <title>debounce</title>
    <style>
      #container {
        width: 100%;
        height: 200px;
        line-height: 200px;
        text-align: center;
        color: #fff;
        background-color: #444;
        font-size: 30px;
      }
    </style>
  </head>

  <body>
    <div id="container"></div>
    <button id="button">点击取消debounce</button>
    <script>
      function debounce(fn, timeout, immediate = false) {
        let timer, result;

        const debouncedFn = function (...args) {
          const context = this;
          if (timer) clearTimeout(timer);

          if (immediate) {
            immediate = false;
            result = fn.apply(context, args);
          }

          timer = setTimeout(fn.bind(context, ...args), timeout);

          return result;
        };

        debouncedFn.cancel = function () {
          clearTimeout(timer);
          timer = null;
        };

        return debouncedFn;
      }

      let count = 1;
      let container = document.getElementById("container");

      function addCount() {
        container.innerHTML = count++;
        return count;
      }

      let debounceFn = debounce(addCount, 1000, true);

      container.onmousemove = function () {
        var res = debounceFn();
        console.log(res);
      };

      document.getElementById("button").addEventListener("click", function () {
        debounceFn.cancel();
      });
    </script>
  </body>
</html>
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69

函数节流

触发高频事件,且 N 秒内只执行一次。

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

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

  return function() {
    var 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 来表示是否可以立即执行一次,opitons.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

手写 new 方法

new 操作符

  1. 首先创建了一个新对象 设置原型
  2. 将对象的原型设置为函数的prototype对象(继承父类原型上的方法)
  3. 让函数的this指向这个对象,执行构造函数的代码(添加父类的属性到新的对象上并初始化)
  4. 判断函数的返回值类型,如果是值类型,返回创建的对象。如果是对象(函数也是对象),就返回这个对象
function myNew(constructor, ...args) {
  if (typeof constructor !== "function") {
      throw "myNew方法的第一个参数必须是一个方法";
  }

  // 基于constructor的原型创建一个全新的对象
  let newObj = Object.create(constructor.prototype);

  // 获取传入的参数
  // let args = Array.from(arguments).slice(1);

  // 添加属性到新创建的newObj上, 并获取obj函数执行的结果
  let result = constructor.apply(newObj, args);

  // 判断result类型,如果是object或者function类型,则直接返回结果
  let resultType = Object.prototype.toString.call(result);
  if (resultType === '[object Object]' || resultType === '[object Function]') {
      return result;
  }
  return newObj;
}

function Person(firtName, lastName) {
  this.firtName = firtName;
  this.lastName = lastName;
}

Person.prototype.getFullName = function () {
  return `${this.firtName} ${this.lastName}`;
};

const a = myNew(Person, 'Chen', 'Jinrui')

console.log(a.getFullName())  // Chen Jinrui
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

手写 instanceof

function Person(name) {
    this.name = name;
}

const jc = new Person("Hello");

console.log(jc instanceof Person);  // true
console.log(jc instanceof Object);  // true
console.log(jc instanceof Array);  // false

function myInstanceOf(obj, origin) {
    if (typeof obj !== 'object' || obj === null) return false;
    while (obj.__proto__ !== null) {
        if (obj.__proto__ === origin.prototype) {
            return true;
        }
        obj = obj.__proto__;
    }
    return false;
}

console.log(myInstanceOf(jc, Person));  // true
console.log(myInstanceOf(jc, Object));  // true
console.log(myInstanceOf(jc, Array));  // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

手写 call 方法

不得使用 apply、bind 函数辅助

Function.prototype.myCall = function (ctx, ...args) {
  ctx = ctx == null ? globalThis : Object(ctx);
  var key = Symbol("fn");
  Object.defineProperty(ctx, key, {
    enumerable: false,
    value: this,  // method.myCall 时 this 即为 method 函数
  });
  var result = ctx[key](...args);
  delete ctx.key;
  return result;
};

function method(a, b) {
  console.log(this, a, b);
  return a + b;
}

console.log(method.myCall(null, 1, 2));  // Object [global] 1 2;  3;
console.log(method.myCall({}, 2, 2));  // {} 2 2;  4
console.log(method.myCall(1, 2, 2));  // [Number: 1] 2 2; 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

手写 bind 方法

Function.prototype.myBind = function (ctx) {
  var fn = this;
  return function (...args) {
    return fn.apply(ctx, args);
  };
};

function fn(a, b) {
  console.log(this, a, b);
  return a + b;
}

const newFn = fn.myBind({});

console.log(newFn(2, 2));  // {} 2 2;  4;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Function.prototype.myBind = function (ctx, ...args) {
  var fn = this;
  return function (...args1) {
    return fn.call(ctx, ...args, ...args1);
  };
};

function fn(a, b) {
  console.log(this, a, b);
  return a + b;
}

const newFn = fn.myBind({}, 1);

console.log(newFn(2));  // {} 1 2;  3;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var slice = Array.prototype.splice

Function.prototype.myBind = function (ctx) {
    var fn = this;
    // 因为arguments 不是数组,没有slice方法,所以用slice.call(arguments)
    var args = slice.call(arguments, 1); // 获取除了第0个参数this,之外的所有其他参数
    if (typeof fn !== "function") {
        throw new Error("bind必须调用在函数上");
    }

    return function () {
        var args2 = slice.call(arguments, 0); // 获取所有参数
        // 用apply传数组,合并两次的参数数组,用apply传
        return fn.apply(ctx, args.concat(args2));
    };
};

function fn(a, b) {
    console.log(this, a, b);
    return a + b;
}

const newFn = fn.myBind({}, 1);

console.log(newFn(2)); // {} 1 2;  3;
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
Function.prototype.myBind = function (ctx, ...args) {
    // this 就是函数
    var fn = this;
    function resultFn(...args2) {
        // 也可以用这句话判断是否用了 new
        // resultFn.prototype.isPrototypeOf(this); 
        // 判断是否用了 new, 对 this 做不同处理
        return fn.call(this instanceof resultFn ? this : ctx, ...args, ...args2);
    }
    // 重新绑定原型
    resultFn.prototype = fn.prototype;
    return resultFn;
};

function fn(a, b) {
    this.a = a;
    this.b = b;
}

const newFn = fn.myBind({}, 1);
console.log(new newFn(2));  // fn { a: 1, b: 2 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var slice = Array.prototype.slice;

Function.prototype.myBind = function (ctx) {
    var args = slice.call(arguments, 1);
    var fn = this;
    if (typeof fn !== "function") {
        throw new Error("bind 必须调用在函数身上");
    }
    function resultFn() {
        var args2 = slice.call(arguments, 0);
        return fn.apply(
            resultFn.prototype.isPrototypeOf(this) ? this : ctx,
            args.concat(args2)
        );
    }
    resultFn.prototype = fn.prototype;
    return resultFn;
};

function fn(a, b) {
    this.a = a;
    this.b = b;
}

const newFn = fn.myBind({}, 3);

console.log(new newFn(4));  // fn { a: 3, b: 4 }
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

手写元素拖拽

const list = document.querySelector(".list");
let sourceNode;
list.ondragstart = (e) => {
    // 拖拽元素样式就是拖拽发生时的样式,立即修改会导致拖拽的也跟着发生变化
    setTimeout(() => {
        e.target.classList.add("moving");
    }, 0);
    sourceNode = e.target;
    // 拖拽的默认行为是复制,可以改成移动
    e.dataTransfer.effectAllowed = "move";
};
list.ondragenter = (e) => {
    e.preventDefault();
    if (e.target === list || e.target === sourceNode) {
        return;
    }
    const children = Array.from(list.children);
    const sourceIndex = children.indexOf(sourceNode);
    const targetIndex = children.indexOf(e.target);
    if (sourceIndex < targetIndex) {
        list.insertBefore(sourceNode, e.target.nextElementSibling);
    } else {
        list.insertBefore(sourceNode, e.target);
    }
};
// 默认行为下很多元素不允许元素拖动到自己身上,导致鼠标放开的时候元素动画回到了原始位置
// 通过阻止 ondragenter、ondragover 的默认行为解决
list.ondragover = (e) => {
    e.preventDefault();
};
list.ondragend = (e) => {
    e.target.classList.remove("moving");
};
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
.list {
    list-style: none;
    margin: 0 auto;
    padding: 0;
    width: 50vw;
}
.list-item {
    background-color: #34495e;
    margin-bottom: 10px;
    cursor: move;
    user-select: none;
    height: 45px;
    line-height: 45px;
    color: white;
    text-align: center;
    border-radius: 4px;
    box-sizing: border-box;
}
.list-item.moving {
    background: transparent;
    color: transparent;
    border: 1px dashed #ccc;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<ul class="list">
    <li draggable="true" class="list-item">1</li>
    <li draggable="true" class="list-item">2</li>
    <li draggable="true" class="list-item">3</li>
    <li draggable="true" class="list-item">4</li>
    <li draggable="true" class="list-item">5</li>
</ul>
1
2
3
4
5
6
7
上次更新: 2022/11/15 10:43:46
贡献者: Jerry Chen, Jinrui Chen