手写|JavaScript 基础系列🤞✌🤞✌

163 阅读10分钟

手写洗牌

function xipai(arr) {
    arr.sort(() => {
            return Math.random().toFixed(1) > 0.5 ? 1 : -1
    })
}
let arr = [1, 2, 3, 4, 5]
xipai(arr)
console.log(arr)
function xipai2(arr) {
	for (let i = 0; i < arr.length; i++) {
		const randomIdx = Math.round(Math.random() * (arr.length - 1 - i)) + i;
        [arr[i], arr[randomIdx]] = [arr[randomIdx], arr[i]]
	}
}
let arr2 = [1, 2, 3, 4, 5]
xipai2(arr2)
console.log(arr2)
let res = []
 function xipai3(arr) {
    let i = 0;
    for (i = arr.length + 1; i > 0;) {
        i--;
        let a = Math.floor(Math.random() * arr.length)
        if (!res.includes(arr[a])) { // 没有重复,进行下一次遍历
            res.push(arr[a])
        } else { // 重复了,那说明随机数无效,在生成一次
            if (res.length === arr.length) {
                break;
            }
            i++;
        }
    }
}
let arr3 = [1, 2, 3, 4, 5]
xipai3(arr3)
console.log(res)

手写时间格式化

/*
 * @Author: Kongjingjing
 * @Date: 2022-09-22 11:07:08
 * @Description: 
 */
function format(time, format) {
    let date = new Date(time);
    let y = date.getFullYear();
    let m = date.getMonth() + 1;
    let d = date.getDate();
    let M = date.getMinutes();
    let H = date.getHours();
    let S = date.getSeconds();
    format = format.replace(/yyyy/, y)
    format = format.replace(/MM/, m)
    format = format.replace(/dd/, d)
    format = format.replace(/MM/, M)
    format = format.replace(/HH/, H);
    format = format.replace(/SS/, S)
    return format;
}
let str = format(new Date().getTime(), 'yyyy年MM月dd日HH时MM分SS秒')
console.log(str);
// 以下都是浏览器输出的结果
console.log(new Date()); // Thu Sep 22 2022 14:46:18 GMT+0800 (中国标准时间)
console.log(new Date().getTime()); // 1663829202791 时间戳 返回的是1970年1月1日到今天的毫秒数
console.log(new Date().toLocaleDateString()); // 2022/9/22
console.log(new Date().toLocaleTimeString()); // 14:47:02
console.log(new Date().toLocaleString()); // 2022/9/22 14:47:18
console.log(new Date().toGMTString()); // Thu, 22 Sep 2022 06:47:31 GMT 少了8个小时
console.log(new Date().toUTCString()); // Thu, 22 Sep 2022 06:48:09 GMT 少了8个小时
console.log(new Date().toISOString()); // 2022-09-22T06:48:40.596Z
console.log(new Date().toJSON()); // 22022-09-22T06:48:54.326Z

手写实现Object.assgin

/*
 * @Author: Kongjingjing
 * @Date: 2022-09-21 10:44:52
 * @Description:
 */
function myAssign(target, ...source) {
  // undefined和null 无法转成对象
  if (target == null) {
    throw new TypeError("Cannot convert undefined or null to object");
  }
  // 强制转成对象
  let res = Object(target);
  source.forEach((ele) => {
    if (ele != null) {
      // 不是空对象
      for (let key in ele) {
        if (ele.hasOwnProperty(key)) {
          res[key] = ele[key];
        }
      }
    }
  });
  return res;
}
let obj = {
  name: "tom",
  age: 18,
};
let myObj = myAssign(
  obj,
  { sex: "男", hobby: "游戏", age: 34 },
  { a: 1, b: 2, c: 3 }
);
console.log(myObj);

手写一个sleep函数

promise

function sleep(time) {
 return new Promise((resolve) => {
   {
     setTimeout(resolve, time);
   }
 });
}
let start = new Date().getTime();
// 睡2000ms之后,在打印1
sleep(2000).then(() => {
 console.log(1);
 let end = new Date().getTime();
 console.log(end - start + "ms"); // 2019ms 但是实际不是2000ms
});
// 因为setTimeout属于宏任务,在执行宏任务之前如何当前执行栈中在执行的任务超过了定时器设定的时间,是可能存在误差的。

构造器

function* sleep(time) {
 yield new Promise((resolve) => {
   setTimeout(resolve, time);
 });
}
let start = new Date().getTime();
sleep(2000)
 .next()
 .value.then(() => {
   console.log(11);
   let end = new Date().getTime();
   console.log(end - start + "ms"); // 2017ms 但是实际不是2000ms
 });

async

function sleep(time) {
 return new Promise((resolve) => {
   setTimeout(resolve, time);
 });
}
let start = new Date().getTime();
async function output() {
 await sleep(2000);
 console.log(22);
 let end = new Date().getTime();
 console.log(end - start + "ms"); // 2019ms 但是实际不是2000ms
}
output();

settimeout

function sleep(callback, time) {
 setTimeout(callback, time);
}
let start = new Date().getTime();
function output() {
 console.log(3333);
 let end = new Date().getTime();
 console.log(end - start + "ms"); // 2020ms 但是实际不是2000ms
}
sleep(output, 2000);

手写浅拷贝

/*
 * @Author: Kongjingjing
 * @Date: 2022-09-20 15:16:33
 * @Description:
 */
function qiancopy(obj) {
  // 只拷贝对象
  if (!obj || typeof obj !== "object") return;
  // 根据obj判断是数组还是对象
  let newObj = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 只拷贝obj的属性,原型上的不考虑
      newObj[key] = obj[key];
    }
  }
  return newObj;
}
let arr = [1, 2, 3];
let arr2 = qiancopy(arr);
console.log(arr2); // [ 1, 2, 3 ]

let obj = {
  name: "tom",
  age: 18,
};

let obj2 = qiancopy(obj); // { name: 'tom', age: 18 }
console.log(obj2);

手写深拷贝

/*
 * @Author: Kongjingjing
 * @Date: 2022-09-20 16:17:06
 * @Description:
 */
function deepCopy(obj) {
  if (!obj || typeof obj !== "object") return;
  let newObj = Array.isArray(obj) ? [] : {};
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // obj的属性又是对象,进行递归
      newObj[key] =
        typeof obj[key] === "object" ? deepCopy(obj[key]) : obj[key];
    }
  }
  return newObj;
}
let obj = { name: "tom", age: 18, propObj: { a: 1, b: 2 } };

let obj1 = deepCopy(obj);
console.log(obj1); // { name: 'tom', age: 18, propObj: { a: 1, b: 2 } }
obj.name = "zhangsan";
console.log(obj); // { name: 'zhangsan', age: 18, propObj: { a: 1, b: 2 } }

console.log(obj1); // { name: 'tom', age: 18, propObj: { a: 1, b: 2 } }

obj.propObj.a = 111;
console.log(obj); // { name: 'zhangsan', age: 18, propObj: { a: 111, b: 2 } }
console.log(obj1); // { name: 'tom', age: 18, propObj: { a: 1, b: 2 } }

手写判断类型函数

手写一个能正确判断类型的函数

1、 考虑处理null
2、考虑处理引用类型
3、考虑处理 基本类型
  if (value === null) {
    return value + ''
  }
  if (typeof value === 'object') {
    // [object Array]
    let arr = Object.prototype.toString.call(value);
    let mid = arr.split(' ')[1].split('');
    // 去掉 ]
    mid.pop();
    return mid.join('').toLowerCase()
  } else {
    return typeof value;
  }
}
console.log(myType(null)); // null
console.log(myType(1)); // number
console.log(myType('1')); // string
console.log(myType(true)); // boolean
console.log(myType(undefined)); // undefined
console.log(myType(function f() { })); // function
console.log(myType([1, 2, 3])); // array
console.log(myType({ name: 'tom' })); // object
console.log(myType(Symbol(1))); // symbol

手写排序

image.png

冒泡排序

比较相邻2个值的大小,每次都把最大的值交换到最后

function bubbleSort(arr) {
    for (let i = 0; i < arr.length; i++) {
        for (let j = 0; j < arr.length - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) { // 相邻2个比大小 交换
                let temp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = temp;
            }
        }
    }
    return arr;
}
let arr = [8, 9, 1, 7, 2, 3, 5, 4, 6, 0]
console.log(bubbleSort(arr));
最好:整个数组有序,遍历n遍,数组就有序了O(n)
最坏: 整个数组反序,O(n^2)
平均: O(n^2)
空间复杂度:O(1)
稳定

选择排序

[无序的数组] ---> [有序的.... 无序的]

默认第一位是最值,然后遍历之后的值,找到最值,进行交换,这样数组的前半部分慢慢变得有序,在从后半部分中依次选择最值,放入到前半部分,最后整个数组有序

function selectSort(arr) {
    for (let i = 0; i < arr.length; i++) {
        let minIndx = i; // 默认第一个值最小
        for (let j = i; j < arr.length; j++) {
            if (arr[j] < arr[minIndx]) { // 遇到了更小的值
                minIndx = j;
            }
        }
        // 最小值交换到有序数列中
        let temp = arr[i];
        arr[i] = arr[minIndx];
        arr[minIndx] = temp;
    }
    return arr;
}
let arr = [8, 9, 1, 7, 2, 3, 5, 4, 6, 0]
console.log(selectSort(arr));
最好:整个数组有序 O(n^2)
最坏: 整个数组反序,O(n^2)
平均: O(n^2)
空间复杂度:O(1)
不稳定
最好、最差、平均时间复杂度都是O(n^2),因为无论你是否完全有序,还是完全逆序,都需要找出后边的最小值进行替换。

插入排序

默认第一个有序,从剩余的无序中选出一个值,将值和有序中的值依次比较,放入正确的位置,依次循环这个过程,最后整个数组有序

function insertSort(arr) {
    for (let i = 0; i < arr.length - 1; i++) {
        let cur = arr[i + 1]; // 当前值 cur之后都是无序的
        let pre = i; // 前一个下标 pre之前都是有序的 
        while (pre >= 0 && arr[pre] > cur) {
            arr[pre + 1] = arr[pre]; // 后移
            pre--;
        }
        arr[pre + 1] = cur; // 将取出的值放入正确的位置
    }
    return arr;

}
let arr = [8, 9, 1, 7, 2, 3, 5, 4, 6, 0]
console.log(insertSort(arr));
最好:整个数组有序,O(n)
最坏: 整个数组反序,O(n^2)
平均: O(n^2)
空间复杂度:O(1)
稳定

希尔排序

image.png

function shellSort(arr) {
    let len = arr.length;
    let gap = parseInt(len / 2); // 增量
    while (gap > 0) {
        let temp;
        for (let i = gap; i < len; i++) {
            let preIndex = i - gap;
            temp = arr[i];
            while (preIndex >= 0 && arr[preIndex] > temp) {
                arr[preIndex + gap] = arr[preIndex];
                preIndex -= gap;
            }
            arr[preIndex + gap] = temp;
        }
        gap = parseInt(gap / 2);

    }
    return arr;
}
let arr = [8, 9, 1, 7, 2, 3, 5, 4, 6, 0]
console.log(shellSort(arr));
最好:
最坏: 整个数组反序,O(nlog2n)
平均: O(n^1.5)
空间复杂度:O(1)
不稳定

归并排序

function mergeSort(array, left, right) {
    if (left === right) return;
    let mid = parseInt(left + (parseInt(right - left) / 2));
    mergeSort(array, left, mid);
    mergeSort(array, mid + 1, right);
    let help = [];
    let i = 0;
    let p1 = left;
    let p2 = mid + 1;
    while (p1 <= mid && p2 <= right) {
        help[i++] = array[p1] < array[p2] ? array[p1++] : array[p2++];
    }
    while (p1 <= mid) {
        help[i++] = array[p1++];
    }
    while (p2 <= right) {
        help[i++] = array[p2++];
    }
    for (let i = 0; i < help.length; i++) {
        array[left + i] = help[i];
    }
    return array;
}
let a = [1, 30, 6, 78, 90, 20];
let len = a.length;
let br = mergeSort(a, 0, len - 1);
console.log(br);
最好:O(n)
最坏: O(nlogn)
平均: O(nlogn)
空间复杂度:O(n)
稳定

快速排序

每次都找出一个值做为基准,然后把数组分成2部分,比基准小的放在前面,比基准大的放在后面。之后在从左右序列分别找出基准,进行排序,递归这个过程。


function quickSort(arr, low, high) {
    if (low < high) {
        let mid = partition(arr, low, high);
        quickSort(arr, low, mid - 1);
        quickSort(arr, mid + 1, high);
    }
}
function partition(arr, low, high) {
    let key = arr[low];
    while (low < high) {
        while (low < high && arr[high] >= key) {
            high--;
        }
        swap(arr, low, high);
        while (low < high && arr[low] <= key) {
            low++;
        }
        swap(arr, low, high);
    }
    return low;
}
function swap(arr, i, j) {
    let temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}

let arr = [1, 30, 6, 78, 90, 20];
let len = a.length;
quickSort(arr, 0, len - 1);
console.log(arr);
最好:O(nlogn)
最坏: O(n^2)
平均: O(nlogn)
空间复杂度:O(logn)
不稳定

手写防抖 节流

防抖: n秒后执行该事件,如果在n秒内被重复触发,则重新计时

节流:n秒内只运行一次,若在n秒内重复触发,只有一次生效

一个经典的比喻:

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应

假设电梯有两种运行策略 debounce 和 throttle,超时设定为15秒,不考虑容量限制

电梯第一个人进来后,15秒后准时运送一次,这是节流。(这是我们常规可以接受的操作)

电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖。(这个就很烦,如果15秒内一直上电梯,那这辈子电梯都不会运行了,也可以说,防抖就看最后一次操作,在15秒内没有发生了,就执行这最后一次操作)

防抖

// 简单版
function debounce(fn, wait) {
    let timer = null;
    return function () {
        let context = this; // 保存this
        let args = arguments; // 保存事件参数
        timer && clearTimeout(timer); // timer!=null

        timer = setTimeout(function () {
            fn.apply(context, args);
        }, wait)
    }
}
// 立即执行版
    function debounce(func, wait, immediate) {
        let timer = null; // 计时器
        let result;
        let debounced = function () {
            let context = this;
            let args = arguments;
            timer && clearTimeout(timer);
            //立即执行
            if (immediate) {
                // 第一次进入,timer的值是null,callNow的值是true,所以立即执行
                // 如果进入之后,鼠标一直在滑动,timer有值,callNow的值是false,所以不会立即执行
                // 等待wait时间以后,设置timer的值是null,鼠标再次移动,立即执行
                let callNow = !timer;
                timer = setTimeout(() => {
                    timer = null;
                }, wait)

                if (callNow) {
                    result = func.apply(context, args);
                }
            } else {
                timer = setTimeout(() => {
                    func.apply(context, args)
                }, wait); // setTimeout的返回值是一个数值
            }
            return result;
        }
        // 取消防抖
        debounced.cancel = function () {
            clearTimeout(timer);
            timer = null;
        }
        return debounced;
    }

节流

// 定时器
function throttle(fn, wait) {
    let timer = null;
    return function () {
        let context = this;
        let args = arguments;
        if (!timer) { // timer是null 说明执行一次
            timer = setTimeout(function () {
                fn.apply(context, arguments)
                timer = null
            }, wait)
        }
    }
}
//时间戳版
    function throttle(fn, delay) {
        //第一时间
        let lastNow = Date.now();
        return function () {
            // 现在
            let now = Date.now();
            //时间间隔已经超过时间间隔了,执行一次函数
            if (now - lastNow >= delay) {
                // 更新第一时间
                lastNow = Date.now();
                return fn.call(this);
            }
        }
    }
// 标志版
    function throttle(fn, delay) {
        let flag = true;
        return function () {
            if (flag) {
                setTimeout(() => {
                    fn.call(this);
                    flag = true;
                }, delay)
            }
            // 执行完标志为false
            flag = false;
        }
    }

参考

# 什么是防抖和节流?有什么区别?如何实现?

手写 Object.create

简单了解创建对象的方式

1.直接定义

let obj1 = {
  name: "tom",
  age: 18,
};
console.log(obj1);

2.new


let obj2 = new Object();
obj2.name = "tom";
obj2.age = 18;
console.log(obj2);

1和2是最常用的创建对象的方式,但是不利于扩展,比如我只是需要一些属性完全相同,只是属性值不同的对象时,这2个方法的弊端就出来了

3.工厂模式

function createPerson(name, age) {
  var obj = new Object();
  obj.name = name;
  obj.age = age;
  return obj;
}
let obj3 = new createPerson("tom", 18);
console.log(obj3);

工厂可以解决1和2的问题,但是工厂创建出来的实例都是Object类型,无法更进一步区分具体类型

4.构造函数

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.getAge = function () {
    return this.name;
  };
}
const person1 = new Person("Tom", 100);
const person2 = new Person("Lihua", 100);

console.log(person1.getAge === person2.getAge); // false

使用构造函数创建的对象可以确定其所属类型,解决了方法 3 中的问题。

但是使用构造函数创建的对象存在一个问题,即不同实例的函数是不一样的

4.基于原型对象的模式

基于原型对象的模式是将所有的函数和属性都封装在对象的 prototype 属性上。

function Person() {
  // 通过 prototype 属性增加属性和函数
  Person.prototype.name = "Tom";
  Person.prototype.age = 100;
  Person.prototype.getAge = function () {
    return this.name;
  };
}
// 生成两个实例
const person1 = new Person();
const person2 = new Person();

console.log(person1.name === person2.name); // true
console.log(person1.getAge === person2.getAge); // true

通过上面的代码可以发现,使用基于原型对象的模式创建的实例,其属性和函数都是相等的,不同的实例会共享原型上的属性和函数,解决了方法 4 存在的问题。

但是该方法也存在一个问题,因为所有的实例会共享相同的属性,那么改变其中一个实例的属性值,便会引起其它实例的属性值的变化,这并不是我们期望的

5.构造函数和原型混合的模式

// 构造函数中定义实例的属性
function Person(name, age) {
this.name = name;
this.age = age;
}

// 原型中添加实例共享的函数
Person.prototype.getAge = function () {
return this.age;
};

// 生成两个实例
const person1 = new Person("Tom", 100);
const person2 = new Person("Lihua", 100);

构造函数和原型混合的模式是目前最常见的创建自定义类型对象的方式。

构造函数中用于定义实例的属性,原型对象中用于定义实例共享的属性和方法,通过构造函数传递参数,这样每个实例都能拥有自己的属性值,同时实例还能共享方法的引用,最大限度的节约了内存的空间,混合模式保存了两者的优点。

6.Object.create()

let obj4 = Object.create({}, { name: { value: "tom" }, age: { value: 18 } });
console.log(obj4);
/**
 * Object.create() 创建一个新的对象,支持2个参数,第二个参数可选
 * Object.create(proto) proto是新对象的原型
 * Object.create(proto, propertiesObject) propertiesObject是新对象的属性
 */
// 创建一个空对象
const o = Object.create(null);
// 空对象的原型是undefined
console.log(o.__proto__ === undefined); // true

const a = { m: 1 };
// a作为b的原型对象
const b = Object.create(a);
console.log(b.__proto__ === a); // true
// 创建一个{} 添加一些属性
const c = Object.create({}, { p: { value: 24 } });
console.log(c);

手写

思路:将传入的参数作为原型

function create(proto) {
  // 构造函数
  function F() {}
  //   构造函数指定原型
  F.prototype = proto;
  // 返回new的结果
  return new F();
}
let obj1 = {
  name: "tom",
  age: 18,
};
let m = create(obj1);
console.log(m);

image.png

手写 instanceof 方法

思路:instanceof用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

1.获取构造函数的原型

2.获取实例对象的原型

3.循环,如果原型是null,没找到,返回false

4.对比两者,相等返回true

5.循环对象原型

// A instanceof B  A是实例对象,B是构造函数
function myInstanceof(left, right) {
  //获取对象的原型
  const proto = Object.getPrototypeOf(left);
  // 获取构造函数的原型
  const prototype = right.prototype;
  while (true) {
    // proto 是null退出
    if (!proto) return false;
    if (proto === prototype) return true;
    // 循环原型
    proto = Object.getPrototypeOf(proto);
  }
}
let arr = [1, 2, 3];
console.log(myInstanceof(arr, Array)); // true
console.log(arr instanceof Array); // true
console.log(myInstanceof(arr, Object)); // true
console.log(arr instanceof Object); // true

手写 new 操作符

// 简单版
function myNew(constructor, ...args) {
  // 定义一个新的对象
  let obj = new Object();
  // 构造函数的原型赋值给新对象原型
  obj.__proto__ = constructor.prototype;
  // 给新对象传入参数
  constructor.apply(obj, args);
  // 返回新对象
  return obj;
}
function Person(name, age) {
  this.name = name;
  this.age = age;
}
let obj = myNew(Person, "tom", 18);
console.log(obj); // Person { name: 'tom', age: 18 }

未命名文件.png

思考💡:new的时候,我们都知道new的是一个构造函数,构造函数也是一个函数,也可以有返回值,我们根据new 的现象推测一下结论

构造函数返回对象

function Person(age) {
  this.age = age;
  return { name: "Jalenl" };
}
 
let obj = new Person(18);
console.log(obj); // {name: 'Jalenl'}

返回非对象

function Person(age) {
  this.age = age;
  return 1;
}
 
let obj = new Person(18);
console.log(obj); // Person {age: 18}


返回{age:18},这么说return失效了,跟没有return一样的结果,那如果没有this绑定内部属性,再返回基本数据类型呢?

没有属性绑定+返回非对象

function Person(){
    return 1
}
new Person()

返回的是一个空对象{},意料之中。

综上,只有构造函数return返回的是一个对象类型时,才能改变初始结果。

所以我们在手写new的时候,复杂版本就需要考虑这个事情了,如果添加完属性值之后是object,说明返回值是对象,那就只能返回构造函数的返回值,如果不是,就返回新对象

// 复杂版
function myNew() {
    // 新建一个空对象
    let obj = null;
    // 构造函数
    let constructor = Array.prototype.shift.call(arguments);
    // 结果值
    let result = null
    // 新对象赋值属性
    result = constructor.apply(obj, arguments);

    return typeof result === 'object' ? result : obj
}

function Person(age) {
    this.age = age;
    return { name: 'tom' }
}
console.log(myNew(Person, 18)); // { name: 'tom' }
console.log(new Person(18)); // { name: 'tom' }

参考👀

创建对象的 7 种方式

深入聊一聊JS中new的原理与实现

两分钟详解Array.prototype.shift.call(arguments)!