JavaScript核心原理(上)

87 阅读7分钟

一、数据类型

1. 主要类型

undefined、null、boolean、string、number、symbol、BigInt、object

2. 存储方式

  • 基础类型存储在栈内存,被引用或被拷贝时,会创建一个完全相等的变量
  • 引用类型存储在堆内存,存储的是地址,多个引用指向同一个地址

3. 判断数据类型

typeof、intanceof、Object.prototype.toString(除object, 都需要call)

  • typeof主要判断基础类型(null除外),引用类型只能判断function
  • intanceof只能判断引用类型

4. 判断数据类型通用方法

function getType(obj) {
    let type = typeof obj
    if(type !== 'object') {
        return type
    }
    
    return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1')
}

二、深浅拷贝

1. 浅拷贝的实现

只拷贝一层属性

1. 浅拷贝方法

Object.assign、concat、扩展运算符、slice

2. 实现思路
  • 对基础类型做一个最基本的一个拷贝;
  • 对引用类型开辟一个新的存储空间,并且拷贝一层对象属性
3. 代码实现
const shallowClone = (target) => {
    if(typeof target === 'object' && target !== null) {
        const cloneTarget = Array.isArray(target) ? [] : {}
        
        for(let prop in target) {
            if(target.hasOwnProperty(prop)) {
                cloneTarget[prop] = target[prop]
            }
        }
        return cloneTarget
    } else {
        return target
    }
}

2. 深拷贝的实现

1. 深拷贝方法

JSON.stringify、递归对象属性

2. 实现思路
  • 针对能够遍历对象的不可枚举属性以及symbol类型,可以使用Reflect.ownkeys方法;
  • 当参数为Date、RegExp类型,则直接生成一个新的实例返回;
  • 利用Object的getOwnPropertyDescriptors方法可以获得对象的所有属性,以及对象的特性,顺便结合Object的create方法创建一个新对象,并继承传入原对象的圆形链;
  • 利用WeakMap类型作为Hash表,因为WeakMap是弱引用类型,可以有效防止内存泄漏,作为检测循环引用很有帮助,如果存在循环,则引用直接返回WeakMap存储的值。
3. 代码实现
const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj!== null)

const deepClone = function(obj, hash = new WeakMap()) {
    // Date、RegExp类型,则直接生成一个新的实例返回
    if(obj.constructor === Date) return new Date(obj)
    if(obj.constructor === RegExp) return new RegExp(obj)
    
    // 使用WeakMap解决循环引用
    if(hash.has(obj)) return hash.get(obj)
    let allDesc = Object.getOwnPropertyDescriptors(obj)
    // 遍历传入参数所有键的特性
    let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)
    // 继承原型链
    hash.set(obj, cloneObj)
    for(let key of Reflect.ownKeys(obj)) {
        cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key]) : obj[key]
    }
    
    return cloneObj
}

// 验证代码
let obj = {
    num: 0,
    str: '',
    bool: true,
    unf: undefined,
    nul: null,
    obj: { name: '我是一个对象', id: 1 },
    arr: [0, 1, 2],
    func: function() { console.log('我是一个函数') },
    date: new Date(0),
    reg: new RegExp('/我是一个正则/ig'),
    [Symbol('1')]: 1
}

let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)

三、继承实现

1. 原型链继承

  • 每一个构造函数都有一个原型对象;
  • 原型对象包含一个执行构造函数的指针;
  • 实例则包含一个原型对象的指针

1. 代码实现

function Parent() {
    this.name = 'parent'
    this.play = [1, 2, 3]
}

function Child() {
    this.type = 'child'
}

Child.prototype = new Parent()

// 验证代码
const s1 = new Child()
const s2 = new Child()
console.log(s1)

2. 缺点

  • 存在内存之间的共享

2. 构造函数继承(借助call)

1. 代码实现

function Parent() {
    this.name = 'parent'
}

Parent.prototype.getName = function() {
    return this.name
}

function Child() {
    Parent.call(this)
    this.type = 'child'
}

// 验证代码
let child = new Child()
console.log(child)
console.log(child.getName())

2. 缺点

  • 不能继承原型属性和方法

3. 组合继承

1. 代码实现

function Parent() {
    this.name = 'parent'
}

Parent.prototype.getName = function() {
    return this.name
}

function Child() {
    // 第二次调用Parent()
    Parent.call(this)
    this.type = 'child'
}

// 第一次调用Parent()
Child.prototype = new Parent()
// 手动挂上构造器,指向自己的构造函数
Child.prototype.constructor = Child

// 验证代码
let s1 = new Child()
let s2 = new Child()
console.log(s1)
console.log(s1.getName())

2. 缺点

  • Parent进行两次构造

4. 原型式继承

使用原型式继承可以获得一份目标对象的浅拷贝

1. 代码实现

let parent = {
   name: 'parent',
   friends: ['p1', 'p2', 'p3'],
   getName: function() {
       return this.name
   }
}

let person = Object.create(parent)
person.name = 'tom'
person.friends.push('jerry')

// 验证代码
console.log(person)
console.log(person.friends)
console.log(person.getName())

2. 缺点

  • 多个属性的引用指向相同内存,存在篡改的可能

5. 寄生式继承

1. 代码实现

let parent = {
   name: 'parent',
   friends: ['p1', 'p2', 'p3'],
   getName: function() {
       return this.name
   }
}

function clone(original) {
    let clone = Object.create(parent)
    clone.getFriends = function() {
        return this.friends
    }
}

let person = clone(parent)

// 验证代码
console.log(person)
console.log(person.getFriends())
console.log(person.getName())

2. 缺点

6. 寄生组合式继承

1. 代码实现

function clone(parent, child) {
    // Object.create可以减少组合继承中多进行一次构造的过程
    child.prototype = Object.create(parent.prototype)
    child.prototype.constructor = child
}

function Parent() {
    this.name = 'parent'
    this.play = [1, 2, 3]
}

Parent.prototype.getName = function() {
    return this.name
}

function Child() {
    Parent.call(this)
    this.friends = 'child'
}

clone(Parent, Child)

Child.prototype.getFriends = function() {
    return this.friends
}

// 验证代码
let person = new Child()

console.log(person)
console.log(person.getFriends())
console.log(person.getName())

四、new、apply、call、bind的原理

1. new的原理

1. new做了什么

  • 创建一个新对象
  • 将构造函数的作用域赋给新对象(this指向新对象)
  • 执行构造函数中的代码(为这个新对象添加属性)
  • 返回新对象(要么是实例对象,要么是return语句指定的对象)

2. new的作用

  • 让实例可以访问到私有属性
  • 让实例可以访问构造函数原型(constructor.prototype)所在原型链上的属性
  • 构造函数返回的最后结果是引用数据类型

3. 代码实现

function _new(ctor, ...args) {
    if(typeof ctor !== 'function') {
        throw new Error('ctor must be a function')
    }
    let obj = new Object()
    obj.__proto__ = Object.create(ctor.prototype)
    let res = ctor.apply(obj, ...args)
    let isObject = typeof res === 'object' && res !== null
    let isFunction = typeof res === 'function'
    
    return isObject || isFunction ? res : obj
}

2. apply、call的原理

1. 使用场景

  • 判断数据类型Object.prototype.toString.call()
  • 类数组借用数组方法
  • 获取数组的最大值/最小值
  • 实现继承

2. 代码实现

// 实现call
Function.prototype.call = function(context, ...args) {
    var context = context || window
    context.fn = this
    var result = eval('contex.fn(...args)')
    delete context.fn
    return result
}

// 实现apply
Function.prototype.apply = function(context, args) {
    var context = context || window
    context.fn = this
    var result = eval('contex.fn(...args)')
    delete context.fn
    return result
}

3. bind的原理

1. 代码实现

Function.prototype.bind = function(context, ...args) {
    if(typeof this !== 'function') {
        throw new Error('this must be a function')
    }
    var self = this
    var fbound = function() {
        self.apply(this instanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments)))
    }
    if(this.prototype) {
        fbound.prototype = Object.create(this.prototype)
    }
    return fbound
}

五、闭包

1. 闭包产生的本质

当前环境中存在指向父级作用域的引用

2. 闭包的表现形式

  • 返回一个函数
  • 作为函数参数传递的形式
  • 立即执行函数(IIFE),创建闭包保存了全局作用域和当前函数的作用域
  • 在定时器、事件监听、Ajax请求、Web Workers或者任何异步中只要使用了回调函数,就是在使用闭包

3. 循环输出问题

for(var i = 0; i <= 5; i++) {
    setTimeout(function() {
        console.log(i)
    }, 0)
}

说明: setTimeout为宏任务,由于JS中单线程eventLoop机制,在主线程同步执行完后才执行宏任务,因此循环结束后,setTimeout中的回调才依次执行。

六、数组原理

1. 类数组

  • 函数里参数对象arguments
  • 用getElementsByTagName/ClassName/Name获得的HTMLCollection(DOM集合)
  • 用querySelector获得的NodeList(节点集合)

2. 数组的扁平化

方法一:普通的递归实现
方法二:利用reduce函数迭代
方法三:扩展运算符
方法四:split和toString共同处理
方法五:ES6中的flat
方法六:正则和JSON方法共同处理

3. 数组的排序方法sort

  • 当n <= 10时, 采用插入排序
  • 当n > 10时,采用三路快速排序
  • 当10 < n <= 1000, 采用中位数作为哨兵元素
  • 当n > 1000时, 每隔200 ~ 215个元素挑出一个元素放到一个新数组中,然后对它排序,找到中间位置的数以此作为中位数

4. push,pop,map,reduce底层实现

1. push 方法的底层实现

关键点就在于给数组本身循环添加新的元素 item,然后调整数组的长度 length 为最新的长度,即可完成 push 的底层实现

Array.prototype.push = function (...items) {
  let O = Object(this);  // ecma 中提到的先转换为对象
  let len = this.length >>> 0;
  let argCount = items.length >>> 0;
  // 2 ^ 53 - 1 为JS能表示的最大正整数
  if (len + argCount > 2 ** 53 - 1) {
    throw new TypeError("The number of array is over the max value")
  }

  for (let i = 0; i < argCount; i++) {
    O[len + i] = items[i];
  }

  let newLength = len + argCount;
  O.length = newLength;

  return newLength;
}
2. pop 方法的底层实现

核心思路在于删掉数组自身的最后一个元素,然后更新最新的长度,最后返回元素的值,即可达到想要的效果。另外就是在当长度为 0 的时候,如果执行 pop 操作,返回的是 undefined,需要做一下特殊处理

Array.prototype.pop = function() {
  let O = Object(this);
  let len = this.length >>> 0;
  if (len === 0) {
    O.length = 0;
    return undefined;
  }

  len--;
  let value = O[len];
  delete O[len];
  O.length = len;

  return value;
}
3. map 方法的底层实现

有了上面实现 push 和 pop 的基础思路,map 的实现也不会太难了,基本就是再多加一些判断,循环遍历实现 map 的思路,将处理过后的 mappedValue 赋给一个新定义的数组 A,最后返回这个新数组 A,并不改变原数组的值

Array.prototype.map = function(callbackFn, thisArg) {
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'map' of null");
  }
  
  if (Object.prototype.toString.call(callbackfn) != "[object Function]") {
    throw new TypeError(callbackfn + ' is not a function')
  }

  let O = Object(this);
  let T = thisArg;
  let len = O.length >>> 0;
  let A = new Array(len);

  for(let k = 0; k < len; k++) {
    if (k in O) {
      let kValue = O[k];
      // 依次传入this, 当前项,当前索引,整个数组
      let mappedValue = callbackfn.call(T, KValue, k, O);
      A[k] = mappedValue;
    }
  }

  return A;
}
4. reduce 方法的底层实现

初始值默认值不传的特殊处理;累加器以及 callbackfn 的处理逻辑

Array.prototype.reduce = function (callbackfn, initialValue) {
  // 异常处理,和 map 类似
  if (this === null || this === undefined) {
    throw new TypeError("Cannot read property 'reduce' of null");
  }

  // 处理回调类型异常
  if (Object.prototype.toString.call(callbackfn) != "[object Function]") {
    throw new TypeError(callbackfn + ' is not a function')
  }

  let O = Object(this);
  let len = O.length >>> 0;
  let k = 0;
  let accumulator = initialValue;  // reduce方法第二个参数作为累加器的初始值

  if (accumulator === undefined) {
    // 抛出异常是在循环完之后,异常是每个数据元素都是空
    throw new Error('Each element of the array is empty');

    // 初始值不传的处理
    for (; k < len; k++) {
      if (k in O) {
        accumulator = O[k];
        k++;
        break;
      }
    }
  }

  for (; k < len; k++) {
    if (k in O) {
      // 注意 reduce 的核心累加器
      accumulator = callbackfn.call(undefined, accumulator, O[k], O);
    }
  }

  return accumulator;
}

七、DOM事件

1. 事件流

事件流分为 3 个阶段:事件捕获、到达目标和事件冒泡

  • 事件捕获最先发生, 为提前拦截事件提供了可能
  • 然后,实际的目标元素接收到事件
  • 最后一个阶段是冒泡,最迟要在这个 阶段响应事件

截屏2023-01-13 下午2.27.56.png

2. 事件委托

const ulItem = document.querySelector("ul");

ulItem.onclick = function (e) {
    e = e || window.event; //这一行及下一行是为兼容IE8及以下版本
    const target = e.target || e.srcElement;
    
    if (target.tagName.toLowerCase() === "li") {
        const list = this.querySelectorAll("li");
        const index = Array.prototype.indexOf.call(list, target);
        alert(target.innerHTML + index);
    }
}