Weekbin

一个厉害的前端新手

LV0
已经发布0篇文章,距离下一等级还需发布1篇文章

JavaScript 浅析(二)

一、对象

1. 对象的基本声明方式

① 创建实例再赋值

复制
let p = new Object()
p.name = 'weekbin'
p.age = 18
p.say = function() {}

② 字面量形式创建

复制
let p = {
  name: 'weekbin',
  age: 18,
  say() {}
}

上面的两个例子式完全等价的,它们的属性合方法都相同;工作中,往往字面量形式更加常用。

2. 对象拓展

① 利用操作符

js 可以直接通过 点操作符 或者 [] 直接添加对象,这两种操作是相同的。

复制
let p = {
  name: 'weekbin',
  age: 18,
  say() {}
}

p.job = 'engineer'
p['job'] = 'engineer'

② 利用 assign

也可以使用 Object.assign() 方法来拓展对象。

复制
let p = {
  name: 'weekbin',
  age: 18,
  say() {}
}

let q = Object.assign(p, { job: 'engineer' })

p === q // true,p 和 q 指向了相同的对象
复制
let p = {
  name: 'weekbin',
  age: 18,
  say() {},
  deep: {
    swim: true,
    fly: false
  }
}

let q = Object.assign({ job: 'engineer' }, p)

p === q // false,p 和 q 指向了不同的对象

q.age = 20

p.age // 18, 不会被更改

q.deep.swim = false

p.deep.swim // false,一同被更改了

Object.assign() 方法返回的对象和 target 是同一个对象。

Object.assign(target, ...sources) 方法,把 sources 上的可枚举属性浅拷贝到了目标 target 上,不是深拷贝

但有时候想在不影响原始对象的情况下,创建一个新对象,既具有原始对象的值和方法也具有自己自定义的值。

③ 利用 JSON

可以利用 JSON 来进行深拷贝:

复制
function Person(){
  this.name = 'weekbin'
  this.age = 18
  this.say(){
    console.log(this.name)
  }
  this.deep = {
    swim: true,
    fly: false
  }
  this.job = null
  this.friends = undefined
  this.girlFriends = 0
  this.boyFriends = '0'
}

let p = new Person()

let q = JSON.parse(JSON.stringify(p))

p === q // false

JSON

虽然说 JSON 可以用来深拷贝,但是只能转化可以转化为 JSON 格式的数据。

  1. 也就是说 function 或者 RegExp 这种不能被转换为 JSON 格式的数据会(部分)丢失,同时,源数据格式中 null 得以保留,而 undefined 也会丢失。

  2. 如果源对象是循环引用的,也不能进行转换,会抛出 Uncaught TypeError: Converting circular structure to JSON 的错误。

  3. 如果仔细看,还会发现实例对象 p 的构造函数指向也被强行变更成了 Object。

  4. 如果源对象存在两个 key 的指向为同一个对象,则经过 JSON 转换之后会丢失关联性,成为两个相同的对象。

const t = { name: 'weekbin' }
const a = {
  key1: t,
  key2: t
}

const b = JSON.parse(JSON.stringify(a)) // 此时 b 中存在 key1 和 key2,其 value 本质上是两个没有关联的对象。

④ 利用自定义函数

原文链接

  1. 要解决循环引用的问题

  2. 要解决相同引用对象的问题

  3. 要考虑不同类型的对象(暂只考虑 array 和 object)

复制
function deepCopy(target) {
  let copyed_objs = [] //此数组解决了循环引用和相同引用的问题,它存放已经递归到的目标对象

  function _deepCopy(target) {
    if (typeof target !== 'object' || !target) {
      return target
    }

    for (let i = 0; i < copyed_objs.length; i++) {
      if (copyed_objs[i].target === target) {
        return copyed_objs[i].copyTarget
      }
    }

    let obj = {}
    if (Array.isArray(target)) {
      obj = [] //处理target是数组的情况
    }
    copyed_objs.push({ target: target, copyTarget: obj })
    Object.keys(target).forEach(key => {
      if (obj[key]) {
        return
      }
      obj[key] = _deepCopy(target[key])
    })
    return obj
  }
  return _deepCopy(target)
}

3. 用工厂函数创建对象

复制
function Human(name, age) {
  const obj = {
    name,
    age,
    say() {
      console.log(`hello ${this.name}`)
    }
  }

  return obj
}

let p = Human('weekbin', 18)

p.name // 'weekbin'
p.age // 18
p.say() // 'hello weekbin'

使用工厂函数创建对象时,每次返回的对象都是一个新对象,也就是说,不存在什么公共的方法与属性。以上例子中,虽然每个 obj 的 say 方法都是相同的,但是它们是 不同的指针引用,本质上就是功能相同的,存在不同内存地址的两个方法。

4. 用类创建对象

复制
function Human(name, age) {
  this.name = name
  this.age = age
}

Human.prototype.say = function () {
  console.log(`hello ${this.name}`)
}

let p = new Human('weekbin', 18)

p.name // 'weekbin'
p.age // 18
p.say() // 'hello weekbin'

以上的例子我们先定义了一个类(Human),然后实例化了一个对象 p,p 具有实例属性 name 和 age,具有原型方法 say。用类创建对象的好处就是,一个类可以实例化 出很多不同的对象,如可以实例化出不同姓名不同年龄段的人,但是它们都可以调用同一个原型方法 say,这样就既满足了创建不同的对象,又满足了使用相同的方法。

二、类

1. 实现方式

JS 中实现类有两种写法,一种是构造函数写法,另一种是 ES6+ 的 class 写法。虽然两种写法不同,但是其本质都是基于构造函数+原型实现继承的。

两种写法各有各的方便之处,并不是 class 形式的写法总是方便的。一般来说,定义一个比较复杂而庞大的类,以构造函数+原型的方式去声明,更利于分文件进行维 护,也方便在原型上动态添加新的共用属性与方法。定义一个相对封闭的类,以 class 的写法更加简洁明了。各有各的好,各有各的秒,灵活使用即可。

以下以两个形式声明一个相同的类以作对比:

构造函数+原型声明

复制
function Human(name, age) {
  this.name = name
  this.age = age
}

Human.prototype.say = function () {
  console.log(`hello ${this.name}`)
}

class声明

复制
class Human {
  constructor(name, age) {
    this.name = name
    this.age = age
  }

  say() {
    console.log(`hello ${this.name}`)
  }
}

以上两种形式同样声明了一个 Human 类,调用 new Human(name, age) 创建新对象时,得到的对象完全相同。

但以class 方式声明的类 Human 不能当作普通函数直接调用,而以构造函数方式直接声明的类可以以普通函数的形式直接调用,此时 this 指向全局对象 global,浏览器中就是 window,从而导致原本应该实例化在对象实例上的值被赋值到了 window 身上。同理,class 上的方法也不能直接当成普通函数被调用。

2. 原理

JS 中要理解类与类继承的原理,其实就是要搞明白 new 发生了什么,实例对象是怎么继承原型对象的方法,并调用构造函数给自己赋值的。

① 原型对象、实例对象、构造函数

首先明确三个名词:原型对象、实例对象和构造函数。

构造函数:也就是上文中的 Human 函数,其为调用其的对象,在其身上添加私有属性。

原型对象:也就是上文中 Human.prototype 的值,其上面描述了实例对象将被继承的公共属性与方法,也描述了构造函数的指向。

实例对象:通俗的讲就是 new 左边的内容,其具有构造函数为其添加的属性,并且能供通过原型链方式访问原型对象的公共属性与方法。

举个例子:

复制
function Human(name, age) {
  // 构造函数 Human
  this.name = name
  this.age = age
}

Human.prototype.say = function () {
  // 原型对象 Human.prototype
  console.log(`hello ${this.name}`)
}

const p = new Human('weekbin', 18) // 实例对象 p

② new 发生了什么

  1. 创建一个新对象

  2. 将其 [[prototype]] 属性指向构造函数的 prototype

  3. 以新对象为上下文调用构造函数为新对象赋值属性与方法

  4. 如果构造函数有返回值则直接返回返回值,如果没有返回值则返回刚刚创建的新对象

举个例子:

复制
function _new(origin, ...args) {
  // 以构造函数的原型为原型创建一个新的对象,步骤一 + 步骤二
  const o = Object.create(origin.prototype)
  // 以新创建的对象o为上下文调用构造函数,添加私有属性与方法,步骤三
  const res = origin.apply(o, args)
  // 如果构造函数有返回值,则直接返回返回值,如果没有返回值,则直接返回新创建的对象,步骤四
  return res ? res : o
}

③ Object.create 原理

  1. 创建一个类

  2. 将其原型指向所给的对象

  3. 以该类创建实例对象

  4. 如果存在第二个参数,则调用 defineProperties 为实例对象进行辅助定义

  5. 返回实例对象

举个例子:

复制
function _create(origin, propertiesObject) {
  function F() {}
  F.prototype = origin
  let res = new F()
  // 此处未进行异常处理,Object.create 的第二个参数如果是 null 或非原始包装对象,则抛出一个 TypeError 异常。
  if (propertiesObject) {
    Object.defineProperties(res, propertiesObject)
  }
  return res
}

④ 回过头来看 Array 和 Object

JS 中,当我们直接声明一个变量时,其实相当于 new 了一个对象。

复制
// 数组
;[1, 2, 3]

new Array(1, 2, 3)

// 对象
{
  name: 'weekbin'
}

new Object({ name: 'weekbin' })

以上两种方式声明的结果是相同的,得到的都是一个对应构造函数的实例对象,实例上存在属性,原型链上存在对应构造函数的原型对象的引用。

3. 继承

通常来说,类与类之间有千丝万缕的关系,可能会遇到一些类需要继承别的类,或者继承多个类的情况。此时仅仅一个构造函数就不够用了,得想办法再声明一个类并且将 原型串起来以达到继承的效果。

① 原型链

以原型链的方式继承的思想其实很简单,就是将子类原型指向父类的实例对象。

复制
function F() {}
F.prototype = {}

function S() {}
S.prototype = new F()

问题:

  1. 直接修改了原型,会导致父类构造函数创造的实例属性变为子类的原型属性(有些时候,我们更希望它是个实例属性)

② 盗用构造函数

盗用构造函数其实就是指,在子类构造函数中直接以子类为上下文调用父类的构造函数,从而达到将原本写在父类实例对象的属性与方法,写在子类的实例对象上。

复制
function F() {}

function S() {
  F.call(this)
}

问题:

  1. 由于盗用了父类构造函数,从而导致了父类构造函数创造的属性与方法都被绑定到了实例对象上(有些属性更希望它在原型上)

  2. 由于没有修改原型链,父类的原型方法会丢失。

③ 组合式继承

组合式继承组合了以上两种方式:子类调用父类的构造函数,子类的原型指向父类的实例。

复制
function F() {}
F.prototype = {}

function S() {
  F.call(this)
}
S.prototype = new F()

问题:

  1. 构造函数被调用了两次

  2. 由于构造函数被调用了两次,所以在实例属性和原型属性上存在重复的属性。

④ 原型式继承

类似于 Object.create(),以一个对象或一个对象的原型为原型来创建一个新的对象。

⑤ 寄生式继承

原型式继承的 plus 版本,原型式继承之后,在新的对象上添加属性与方法。

⑥ 寄生式组合继承

原型式继承父类原型,新对象构造函数指向子类,子类原型指向新对象。

复制
function inheritPrototype(sub, sup){
  let prototype = Object.create(sup.prototype)
  prototype.constructor = sub
  sub.prototype = prototype
}

function F() {}
F.prototype = {}

function S() {
  F.call(this)
}

inheritPrototype(S, F)

与纯粹的组合式继承相比,构造函数只调用了一次,从而实例属性与原型属性不会存在重复。ES6 中 class extends 的行为,也正是寄生式组合继承的行为。

4. class

在 ES6+ 中可以用 class 关键字来声明类,其实本质还是原型链的玩法。其中多了 super 关键字可以在子类中调用父类方法或者父类构造函数。还多了 static 关键字, 保证某些方法只能够在类当中调用。

5. 总结

JS 中通过构造函数+原型的形式来创造一个类。以获得构造函数创造的属性与方法+共享原型上的属性与方法来实现继承。

三、Function 和 Object 到底是谁构造了谁?

① Function 和 Object 有关系吗?

有,Function 的原型是由 Object 构造的,Object 是由 Function 构造的。

Object 本身就是个构造函数,那它就具有函数的特征。同时也是 Function 的实例对象,也是个对象。


② Function instanceof Object 为 true,Object instanceof Function 也为 true?

不仅如此,不妨更迷幻一点:

Function 和 Object 是谁构造了谁

由结论一可知,在 Function 和 Object 的 [[prototype]] 属性上不停的找下去,都能找到对方的 prototype 属性。


③ JS 中万物皆对象?

看具体怎么理解。

Function 的确也具有 Object 的一些特征。不妨说,js 中任何对象,不停地找 [[prototype]] 值,最终都会指向 Object.prototype,Object.prototype.proto 指 向 null,最终都会指向 null。

则,每个对象都有 [[prototype]] 属性,但不一定有 prototype 属性,不停地向下找 [[prototype]] (proto),确实是有头有尾的。


1. Function 是构造函数还是对象?

JS 中 Object 和 Function 是两个比较特殊的东西,首先借助浏览器来看下 Function 和 Object:

Function 和 Object 是谁构造了谁

通过 Firefox 打印出 Function 可以看到,Function 具有 prototype 属性,也具有 [[prototype]] 属性,同时满足 Function.proto === Function.prototype 的 结果为 true。这也就是 Function 最特别的地方,就好像是 Function = new Function 才有的行为,所以有了自己构造自己的现象。

其次,Function.prototype.proto 指向了 Object.prototype ,可以理解为 Function 的原型是由 Object 构造的。

再来看 Object 的原型:

Function 和 Object 是谁构造了谁

Object.prototype 没有 prototype ,proto也指向了 null,Object 的原型不由任何东西构造,暂且称其为 源型,下文也称 Object.prototype 为 源型。

至此 Function.proto.proto.proto 为 null,一直拿 [[prototype]] 属性的终点就是 null。

综上,可以这样理解,Function 自己构造了自己,Function 的 prototype 是由 Object 构造的。

2. Object 是构造函数还是对象?

Function 和 Object 是谁构造了谁

Object 的 [[prototype]] 指向了 Function.prototype,可以理解为 Object 本身是由 Function 构造出来的,但有意思就有意思在,Function.prototype.proto 指 向了 Object.prototype,可以理解为,Function 的原型是由 Object 构造出来的。所以有意思就有意思在这个地方。

为帮助理解,不妨假设先有的 源型,用其构造 Function 的 prototype,于是 Function 具有 prototype,就像构造函数一样,于是构造自己,于是就有了 Function.proto === Function.prototype 。然后用 Function 去构造 Object,于是 Object 的 [[prototype]] 属性就指向了 Function 的 prototype,此时,再将 Object 的 prototype 也指向 源型。于是就有了现在的现象。

综上 Function 和 Object 的确存在循环引用。

3. instanceof

instanceof 用于检测构造函数的 prototype 是否出现在某个实例对象上。按我的理解:就是检测 实例对象的 [[prototype]] 属性是否指向构造函数的 prototype,如果 没找到,就尝试找实例对象的 [[prototype]] 的 [[prototype]]。简单来说就是拿构造函数 A.prototype 和 A 的实例对象 a.proto,a.proto.proto,a.proto.proto.proto 一直尝试比较,一直到指向 null 停下。

Function 和 Object 是谁构造了谁

都能找到,所以 instanceof 都返回了 true。

up-to-top