【js 知识】js 常见知识点

89 阅读4分钟

前言

  • 介绍一下 js 的一些基础知识,适合初学者学习,一些刚刚毕业的学生面试中可能会经常被问到这些问题

实现深拷贝

  • 实现一个深拷贝,这边使用 WeakMap 来解决循环引用的问题;
let weakMap = new WeakMap()
function deepClone(obj) {
  if (obj === null) {
    return obj
  }
  if (typeof obj !== 'object') {
    return obj
  }
  if (obj.constructor === Date) return new Date(obj)
  if (weakMap.has(obj)) {
    return weakMap.get(obj)
  }
  let newObj = new obj.constructor() // 拿到原型对象上的属性
  weakMap.set(obj, newObj)
  for (key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] =
        typeof obj[key] === 'object' ? deepClone(obj[key]) : obj[key] // 此处可以用 arguments.callee() 实现
    }
  }
  return newObj
}

判断数组的几种方法

  1. instanceof

    • 判断 Array.prototype 是否出现在其原型链上的任何位置
    • 查找逻辑:a 的 __proto__ 一层一层向上找,看能否找到 Array.prototype
    let a = []
    a instanceof Array //true
    let b = {}
    b instanceof Array //false
    
    • 实现一个 instanceof
    function _instanceof(l, r) {
      l = l.__proto__
      r = r.prototype
      while (true) {
        if (l === null) {
          return false
        }
        if (l === r) {
          return true
        }
        l = l.__proto__
      }
    }
    
    • 存在问题 1:如果网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,iframe 会产生新的全局环境,它也会拥有自己的 Array.prototype 属性;如果你从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数;
    //为body创建并添加一个iframe对象
    let iframe = document.createElement('iframe')
    document.body.appendChild(iframe)
    //取得iframe对象的构造数组方法
    xArray = window.frames[0].Array
    //通过构造函数获取一个实例
    let arr = new xArray(1, 2, 3)
    arr instanceof Array //false
    
    • 存在问题 2:可以改变 __proto__ 的指向
    let a = {}
    a.__proto__ = []
    a instanceof Array //true
    
  2. constructor

    • 实例的构造函数属性 constructor 指向构造函数
    let arr = []
    arr.constructor === Array // true
    
    • 存在问题 1:同样存在上面的全局变量问题
    • 存在问题 2:构造函数的指向可以改变
    let str = 'breeze';
    str.__proto__constructor = Array;
    str.constructor === Array; // true
    
  3. Object.prototype.toString.call()

    • 不光可以判断数组,可以判断任意类型的值
    let a = []
    let b = {}
    let c = function () {}
    Object.prototype.toString.call(a) // '[object Array]'
    Object.prototype.toString.call(b) // '[object Object]'
    Object.prototype.toString.call(c) // '[object Function]'
    
  4. Array.isArray()

    • 判断是否为数组,返回布尔值,es5 提出,之前版本不支持
    let a = [1, 2, 3]
    Array.isArray(a) //true
    

实现一个简单的 promise

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
function PromiseZ(fn) {
  this.status = PENDING
  this.value = undefined
  this.reason = undefined
  this.onFulfilledCallback = [] // 需要在then方法里赋值
  this.onRejectedCallback = [] // 需要在then方法里赋值
  const me = this
  function resolve(value) {
    if (me.status === PENDING) {
      me.status = FULFILLED
      me.value = value
      queueMicrotask(() => {
        me.onFulfilledCallback.forEach((fun) => {
          fun(value)
        })
      })
    }
  }
  function reject(reason) {
    if (me.status === PENDING) {
      me.status = REJECTED
      me.reason = reason
      queueMicrotask(() => {
        me.onRejectedCallback.forEach((fun) => {
          fun(reason)
        })
      })
    }
  }
  try {
    fn(resolve, reject)
  } catch (e) {
    reject(e)
  }
}
function resolvePromise(promise, res, resolve, reject) {
  if (promise === res) {
    //防止循环引用
    return reject(new TypeError('循环引用'))
  }
  let called //防止重复执行
  if (res !== null && (typeof res === 'function' || typeof res === 'object')) {
    try {
      //防止promise执行报错
      let then = res.then //判断是否promise就判断是否存在then方法
      if (typeof then === 'function') {
        //如果返回的是promise,只需要在返回的promise的then方法中下一步需要执行的函数
        then.call(res, (res2) => {
          if (called) return
          called = true
          resolvePromise(promise, res2, resolve, reject) //如果是promise继续递归执行,直到不是promise,依次执行外层的resolve,让promise状态改变
        })
      } else {
        //如果不是promise,有可能是undefine、onfulfilled或onrejected的返回的普通值,就直接将这个值返回,将外层的promise状态改变
        if (called) return
        called = true
        resolve(then)
      }
    } catch (e) {
      if (called) return
      called = true
      reject(e)
    }
  } else {
    resolve(res)
  }
}
PromiseZ.prototype.then = function (onFulfilled, onRejected) {
  const me = this
  const onFulfilledCallback =
    typeof onFulfilled === 'function' ? onFulfilled : (value) => value
  const onRejectedCallback =
    typeof onRejected === 'function'
      ? onRejected
      : (reason) => {
          throw reason
        }
  let x = null
  let promise2 = new PromiseZ((resolve, reject) => {
    if (me.status === FULFILLED) {
      queueMicrotask(() => {
        x = onFulfilledCallback(me.value)
        resolvePromise(promise2, x, resolve, reject)
      })
    } else if (me.status === REJECTED) {
      queueMicrotask(() => {
        x = onRejectedCallback(me.reason)
        resolvePromise(promise2, x, resolve, reject)
      })
    } else {
      me.onFulfilledCallback.push((value) => {
        try {
          x = onFulfilledCallback(value)
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      })
      me.onRejectedCallback.push((reason) => {
        try {
          x = onRejectedCallback(reason)
          resolvePromise(promise2, x, resolve, reject)
        } catch (e) {
          reject(e)
        }
      })
    }
  })
  return promise2
}
let p = new PromiseZ((res) => {
  res(11)
})
p.then((value) => {
  return new PromiseZ((res) => {
    res(value)
  })
}).then((value) => {
  console.log(value)
})

继承

利用构造函数实现继承

  • 利用 call 拿到父构造函数的 this;
function Parent () {
    this.name = 'breeze',
    this.age = 18
}
function Child () {
    Parent.call(this)
    this.address = 'shanghai'
}
var test = new Child1()
  • 缺点是如果 Parent 在原型链上面添加方法或者属性,是无法继承的。

利用原型链来实现继承

  • 利用 prototype 的方法来实现继承
function Parent () {
    this.name = 'breeze',
    this.age = 18,
    this.offer = [1, 2, 3]
}
Parent.prototype = {
  say () {
    console.log('say')
  }
}
function Child () {
    this.address = 'shanghai'
}
Child.prototype = new Parent()
let test1 = new Child();
let test2 = new Child();
  • 缺点是如果改变了引用地址的值,所以的继承都会改变,如下:
let test1 = new Child();
let test2 = new Child();
test1.offer.push(4);
console.log(test1.offer) // [1, 2, 3, 4]
console.log(test2.offer) // [1, 2, 3, 4] (希望大家都能多多的拿 offer)

组合继承

  • 利用 call 和 prototype 来实现继承
function Parent () {
    this.name = 'breeze',
    this.age = 18
}
function Child () {
    Parent.call(this)
    this.address = 'shanghai'
}
Child.prototype = new Parent()
let test1 = new Child();
let test2 = new Parent();
console.log(test1.constructor) // Parent
console.log(test2.constructor) // Parent
  • 缺点:分不清构造函数是哪一个了,还有两次 new Parent() 性能浪费
组合继承优化
Child.prototype = new Parent()
替换成 Child.prototype = Parent.prototype
  • 缺点是依旧存在构造函数分不清的错误
  • 原因是因为因为 Child.prototype = Parent.prototype,而 Parent.prototype 的 constructor 肯定是指向自己的;

组合继承

  • 利用 Object.create 来实现继承
function Parent () {
    this.name = 'breeze',
    this.age = 18
}
function Child () {
    Parent.call(this)
    this.address = 'shanghai'
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child // 注意这边的构造函数,如果不加的话还是 Parent
let test1 = new Child();
  • 值得说一下的是组合继承是没有办法通过 Child.prototype.constructor = Child 来改变构造函数的,因为这样会造成 Parent 的混乱。