JS 基础知识点

typeof vs instanceof

typeof 对于原始类型来说,除了null都可以显示正确的类型

typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'

```javascript
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'

如果我们想判断一个对象的正确类型,这时候可以考虑instanceof,因为内部机制是通过原型链来判断的。

const Person = function() {}
const p1 = new Person()
p1 instanceof Person // true

ar str = 'hello world'
str instanceof String // false

var str1 = new String('hello world')
str1 instanceof String // true

对于原始类型来说,想直接通过instanceof判断类型是不行的,当然我们还是有办法让instanceof判断原始类型的

class PrimitiveString {
  static [Symbol.hasInstance](x) {
    return typeof x === 'string'
  }
}
console.log('hello world' instanceof PrimitiveString) // true

你可能不知道Symbol.hasInstance是什么东西,其实就是一个能让我们自定义instanceof行为的东西,以上代码等同于 typeof 'hello world' === 'string',所以结果自然是 true 了。这其实也侧面反映了一个问题, instanceof 也不是百分之百可信的。

深浅拷贝

我们了解对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况。通常在开发中我们不希望出现这样的问题,我们可以使用浅拷贝来解决这个情况。

let a = {
    age: 1
}

let b = a

a.age = 2

console.log(b.age) // 2

浅拷贝

首先可以通过Object.assign来解决这个问题,很多人认为这个函数是用来深拷贝的。 其实并不是,Object.assign只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。

let a = {
  age: 1
}
let b = Object.assign({}, a)
a.age = 2
console.log(b.age) // 1

另外我们还可以通过展开运算符 ... 来实现浅拷贝

let a = {
  age: 1
}
let b = { ...a }
a.age = 2
console.log(b.age) // 1

通常浅拷贝就能解决大部分问题了,但是当我们遇到如下情况就可能需要使用到深拷贝了

let a = {
  age: 1,
  jobs: {
    first: 'FE'
  }
}
let b = { ...a }
a.jobs.first = 'native'
console.log(b.jobs.first) // native

浅拷贝值解决了第一层的问题,如果接下去的值中还有对象的话,那么就又回到最开始的话题了,两者共享相同的地址,要解决这个问题,我们就得使用深拷贝了。

深拷贝

这个问题通常可以通过JSON.parse(JSON.stringify(object))来解决

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}

let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE

但是该方法也是有局限性的:

  • 会忽略undefined
  • 会忽略symbol
  • 不能序列化函数
  • 不能解决循环引用的对象

在遇到函数undefined 或者 symbol 的时候,该对象也不能正常的序列化

let a = {
  age: undefined,
  sex: Symbol('male'),
  jobs: function() {},
  name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}

你会发现在上述情况中,该方法会忽略掉函数和undefined

但是在通常情况下,复杂数据都是可以序列化的,所以这个函数可以解决大部分问题。

如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel

function structuralClone(obj) {
  return new Promise(resolve => {
    const { port1, port2 } = new MessageChannel()
    port2.onmessage = ev => resolve(ev.data)
    port1.postMessage(obj)
  })
}

var obj = {
  a: 1,
  b: {
    c: 2
  }
}

obj.b.d = obj.b

// 注意该方法是异步的
// 可以处理 undefined 和循环引用对象
const test = async () => {
  const clone = await structuralClone(obj)
  console.log(clone)
}
test()

当然你可能想自己来实现一个深拷贝,但是其实实现一个深拷贝是很困难的,需要我们考虑好多种边界情况,比如原型链如何处理、DOM 如何处理等等,所以这里我们实现的深拷贝只是简易版。

function deepClone(obj) {
    // 判断是否是对象
    function isObject(o) {
        return (typeof o === 'object' || typeof o === 'function') && o !== null
    }

    if (!isObject(obj)) {
        throw new Error('非对象')
    }

    // 判断是否是数组
    let isArray = Array.isArray(obj)
    let newObj = isArray ? [...obj] : {...obj}
    // 返回一个由目标对象自身的属性键组成的数组
    // Reflect.ownKeys
    Reflect.ownKeys(newObj).forEach(key => {
        newObj[key] = isObject(obj[key]) ? deepClone(obj[key]) : obj[key]
    })

    return newObj
}


let obj = {
  a: [1, 2, 3],
  b: {
    c: 2,
    d: 3
  }
}
let newObj = deepClone(obj)
newObj.b.c = 1
console.log(obj.b.c) // 2

手写 call、apply 及 bind 函数

首先从以下几点来考虑如何实现这几个函数

  • 不传入第一个参数,那么上下文默认为window
  • 改变了this指向,让新的对象可以执行该函数,并能接受参数

那么我们先来实现call

Function.prototype.myCall = function (context) {
    if (typeof this !== 'function') {
        throw new TypeError('Error')
    }

    // context 为可选参数,如果不传的话默认上下文为 window
    context = context || window
    // 给 context 创建一个 fn 属性,并将值设置为需要调用的函数
    context.fn = this
    // 因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
    const args = [...arguments].slice(1)

    // 然后调用函数并将对象上的参数删除
    const result = context.fn(...args)
    delete context.fn

    return result
}

以下是对实现的分析:

  • 首先 context 为可选参数,如果不传的话默认上下文为 window
  • 接下来给 context 创建一个 fn 属性,并将值设置为需要调用的函数
  • 因为 call 可以传入多个参数作为调用函数的参数,所以需要将参数剥离出来
  • 然后调用函数并将对象上的函数删除

以上就是实现 call 的思路,apply 的实现也类似,区别在于对参数的处理,所以就不一一分析思路了

Function.prototype.myApply = function (context) {
    if (typeof this !== 'function') {
        throw new TypeError('Error')
    }

    context = context || window
    context.fn = this

    let result

    // 处理参数和call有区别
    if (arguments[1]) {
        result = context.fn(...arguments[1])
    }
    else {
        result = context.fn()
    }

    delete = context.fn()

    return result
}

bind 的实现对比其他两个函数略微地复杂了一点,因为 bind 需要返回一个函数,需要判断一些边界问题,以下是 bind 的实现

Function.prototype.myBind = function (context) {
    if (typeof this !== 'function') {
        throw new TypeError('Error')
    }

    const _this = this
    const args = [...arguments].slice(1)

    // 返回一个函数
    return function F() {
        // 因为返回了一个函数,我们可以使用 new F(),所以需要判断
        if (this instanceof F) {
            return new _this(...args,...arguments)
        }
        return _this.apply(context,args.concat(...arguments))
    }

}

以下是对实现的分析:

  • 前几步和之前的实现大相径庭,就不赘述了
  • bind 返回了一个函数,对于函数来说有两种方式调用,一种是直接调用,一种是通过new的方式,我们先来说直接调用的方式
  • 对于直接调用来说,这里选择了apply的方式实现,但是对于参数需要注意一下情况:因为bind可以实现类似这样的代码f.bind(obj, 1)(2),所以我们需要将两边的参数拼接起来,于是就有了这样的实现args.concat(...arguments)
  • 最后来说通过new的方式,不会被任何方式改变 this,所以对于这种情况我们需要忽略传入的 this