[你不知道的JavaScript(上)] this和原型对象读书笔记

256 阅读18分钟

关于this

为什么要用this

如果我们不用this,我们需要显式传入上下文对象,比如

function speak(content) {
    console.log(`hello, i am ${content.name}`)
}

const person = {name: 'your baba'}

speak(person)

那么,如果我们使用this,则可以这么写

function speak(content) {
    console.log(`hello, i am ${this.name}`)
}

const person = {name: 'your baba'}

speak(person)

this提供了一种更加优雅的方式“隐式”的传递一个对象,因此API的设计会更加简洁,如果显式传递上下文,随着项目越来越复杂会变得越来越混乱。

消除误解

  1. this指向自身。举一个简单的栗子
function foo(sum) {
    console.log(`foo ${sum}`)
    
    this.count++
}
foo.count = 0

for (let i = 1; i <= 10; i++) {
    foo(i)
}
console.log(foo.count) // 0

执行foo.count = 0时确实给foo添加了一个count的属性,且值为0,但是很明显,函数内的this并没有指向函数本身。至于为啥,暂且不说,请往后看~~

  1. 指向它的作用域。这一点并不是所有时候都是错的,在某些时候确实是对的。但是我们不能这么去理解this,this这个概念有点绕,如果我们一开始切入点就错了的话我们会变得很难去理解this这个概念。栗子。
function foo() {
    var a = 2
    this.bar()
}
function bar() {
    console.log('are you ok?')
}
foo()  // are you ok?
// 另外一个栗子
const obj = { foo }
obj.foo() //TypeError: this.bar is not a function

我们可以看到都是调用foo函数,两次得到的结果却不一致,所以this并不是指向函数的作用域。

那么问题来了,this到底是啥玩意

this是在运行时绑定的,而不是编写时绑定的,它的上下文取决于它调用时的各种条件。函数在调用时,会创建一个活动记录,我们称之为执行上下文,包含了函数在哪被调用(调用栈),函数的调用方式、传入的参数等信息,this就是这个记录的其中的一个属性,在函数的执行过程会被用到。

this全面解析

调用位置

理解this之前,必须了解什么叫做调用位置,举个🌰

function first() {
    second() // second的调用位置
}
function second() {
    third() // third的调用位置
}
function third() {
    console.log('are you ok ?')
}
first() // first的调用位置,调用链 first -> second -> third

// 另外一个🌰

const obj = {
    are: {
        you: {
            ok() {
                console.log('im very ok')
            }
        }
    }
}
obj.are.you.ok() // 调用链是 obj -> are -> you -> ok, ok的直接调用者是you
const areYouOk = obj.are.you.ok
areYouOk() // 这里的直接调用者就是window了

调用位置和定义的位置是没有关联的,我们只需要通过调用位置找出直接调用者就好了,这决定了this的绑定

绑定规则

我们首先要找到调用位置,然后通过判断要应用以下哪一种方式。

默认绑定

独立函数调用是最常见的函数调用,作为不适合其他规则时的默认规则。

function foo() {
    console.log(this.a)
}

var a = 'hello'

foo() // hello

function bar() {
    var a = 'world'
    foo() // 大家可以思考一下这里输出啥
}
bar()

这里调用foo的时候就是用了默认绑定,因此指向了全局变量。直接使用不带任何修饰符的函数引用进行调用的,就是默认绑定。 但是需要注意的一点就是,如果定义时开启了严格模式,是不能将全局对象引用到默认绑定的。

function foo() {
    "use strict";
    console.log(this) 
}
foo() //undefined

但是调用的时候是不影响的

function foo() {
    console.log(this)
}
;(function () {
    'use strict'
    foo() // Window
})()

隐式绑定

这种绑定需要我们考虑调用位置是否有上下文。举个🌰

const obj = {
    are: 'you',
    ok() {
        console.log(this.are, 'are very ok')
    },
}

obj.ok() //you are very ok

这里调用位置是obj上下文来调用ok函数,当函数有上下文引用时,隐式绑定就会把函数中的this绑定到这个上下文,那么通过这种调用,this.areobj.are的效果是一样的。而且,这种隐式调用只会绑定直接调用函数的上下文,举个🌰

const obj = {
    a: 'im very ok',
    are: {
        a: 'im very happy to ... to be an Indian',
        you: {
            a: 'do you like me?',
            ok() {
                console.log(this.a)
            },
        },
    },
}

obj.are.you.ok() // do you like me?

隐式丢失

隐式绑定是可以丢失的,我们可以通过隐形丢失来绑定到全局对象或者undefined上(取决于是否严格模式)。我们再重温一遍,直接使用不带任何修饰符的函数引用进行调用的,就是默认绑定。 一定一定一定记得这句话噢,上面留的坑也是通过这句话来找出答案哒。

const obj = {
    a: 'im very ok',
    are: {
        a: 'im very happy to ... to be an Indian',
        you: {
            a: 'do you like me?',
            ok() {
                console.log(this.a)
            },
        },
    },
}
var a = 'hello thank you'
const areYouOk = obj.are.you.ok

areYouOk() // hello thank you

显示绑定

隐式绑定是用过上下文的引用来改变this的指向。那么有没有方法强行改变this指向呢?答案当然是有的咯,而且是面试的常客了,就是applycallbind这三兄弟了。apply和call功能上是完全一致的,只是参数不一样,两者的第一个参数都是需要绑定的对象,而区别在于apply的第二个参数是函数执行需要的参数数组,而call则是一个个独立的参数,给大家介绍个区分的方法,就是apply和array都是a开头的,所以apply的参数是数组,那么排除法call就是独立参数啦。

const obj = {
    a: '德玛西亚万岁~~~'
}
function foo(name, sex) {
    console.log(this.a, name, sex)
}
foo.apply(obj, ['盖伦', '男']) // 德玛西亚万岁~~~ 盖伦 男
foo.call(obj, '卡特琳娜', '女') // 德玛西亚万岁~~~ 卡特琳娜 女

额外需要注意的一点就是当我们将null/undefined作为值传进去的时候,得到的是window,当前js运行环境的全局对象,node是global全局对象。

bind和其他两个函数不同之处是它其实会返回一个函数,而不是像那两个直接执行, 参数跟call一样,后面传的参数列表是作为函数的预置参数,且不可改动。


const obj = {
    a: '德玛西亚万岁~~~',
}
function foo(name, sex) {
    console.log(this.a, name, sex)
}

const bind = foo.bind(obj, '诺手')
bind('男') // 德玛西亚万岁~~~ 诺手 男
bind('剑魔', '没有性别') // 德玛西亚万岁~~~ 诺手 剑魔  这里无法改变预置的参数

new绑定

一般来说在其他语言里,new 会执行类里面的构造函数,JavaScript也有new关键字,但是跟其他语言的new可不太一样。JavaScript的构造函数只是被new执行的函数,它不一定属于某个类,甚至严格来说js没有真正的类,所以也不会实例化一个类,构造函数就是一个普通函数,只是被new 执行。

用new来执行函数,会进行一下几步操作。

  1. 创建一个全新的对象
  2. 新对象执行Prototype连接。
  3. 新对象绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么就返回这个对象 写成代码就是
function myNew(fn, ...args) {
    function isObject(value) {
        return value && typeof value === 'object'
    }
    const instance = Object.create(fn.prototype)
    const result = fn.apply(instance, args)
    return isObject(result) ? result : instance
}

function foo(a, b) {
    this.a = a
    this.b = b
}

const obj1 = myNew(foo, 5, 6)
const obj2 = new foo(5, 6)
console.log(obj1, obj2) // foo{} foo{}

所以new绑定就是创建一个新的对象,然后把他绑定到函数调用的this上。

优先级

然后我们需要了解的一个点就是优先级的问题,上面四种如果同时出现的时候this会绑定到哪个上面。

  1. 默认绑定:这个优先级最低应该没啥异议吧。
  2. 显式绑定和隐式绑定,我们可以通过🌰直观看出谁的优先级高
        function foo() {
            console.log(this.a)
        }
        const obj1 = {
            a: 'hello',
            foo
        }
        const obj2 = {
            a: 'world'
        }
        obj1.foo.call(obj2) // world
    
    所以我们可以看出显式绑定优先级比隐式的高。
  3. new和隐式绑定也是同理
        function foo(a) {
            this.a = a
        }
        const obj1 = {
            a: 'hello',
            foo
        }
        const bar = new obj1.foo('world')
        console.log(bar.a) // world
        console.log(obj1.a) // hello
    
  4. 来到决赛圈,new 和 显式绑定。实际上new和call、apply是不能同时使用的。所以我们可以通过迂回的方式,也就是bind来判断一下优先级的问题。
        function foo(a) {
            this.a = a
        }
    
        const obj = { a: 'hello' }
    
        const bar = new (foo.bind(obj))('world')
        console.log(bar.a) // world
    

所以最终的胜利者是new!!!默认绑定独一档,其他的 new > 显示绑定 > 隐式绑定。 所以判断this可以按照这个顺序来判断

  1. 是否有new, 有的话new绑定,this指向新创建的对象
  2. 是否有apply、call、bind?有的话this绑定指定的对象
  3. 是否有上下文调用?有的话this绑定在函数的直接调用者的上下文。
  4. 如果都没有,那么就是默认绑定咯。

特殊情况

  1. 把null或undefined作为第一个参数传入bind、apply、call。那么这个参数会被忽略,实际上会实行默认绑定。

        const obj1 = {
            a: 'hello',
            foo: function() {
                console.log(this.a)
            }
        }
        var a = 'world'
        obj1.foo.call(null) //world
    
  2. 间接绑定

    function foo() {
        console.log(this.a)
    }
    var a = 2
    var o = {
        a: 3,
        foo
    }
    var p = {
        a: 4
    }
    o.foo() // 3
    (p.foo = o.foo)() // 2
    

    这里p.foo = o.foo执行结果是被引用的对象,也就是foo,所以调用位置是foo()而不是o.foo或者p.foo,所以this指向全局变量。

  3. 箭头函数。箭头函数本身是没有this的,而且也不能使用new执行。箭头函数的this绑定在其执行上下文,不会被修改。举个🌰。

        function foo() {
            return () => console.log(this.a)
        }
        const obj1 = {
            a: 5,
        }
        const obj2 = {
            a: 6,
        }
    
        const bar = foo.call(obj1)
    
        bar.call(obj2) // 5 而不是6,上下文一旦确定就无法修改this的指向
        
        // 其他例子
        const obj = {
            a: 1,
            foo() {
                return () => console.log(this.a)
            },
        }
    
        var a = 2
    
        obj.foo()() // 1
        const bar = obj.foo
        bar()() //2
    
    

对象

语法

对象有两种形式定义。

  • 声明形式(字面量)
        const obj = {
            a: 1
        }
    
  • 构造形式
        const obj = new Object()
        obj.a = 1
    

一般来说是用字面量的形式进行定义比较多。

类型

JavaScript有如下基础类型stringnumberbooleansymbolnullundefinedbigint。复杂类型只有一个就是object。当然复杂类型object下又细分了很多个子对象,通常也称之为内置对象,比如FunctionArrayErrorDateRegExpStringBooleanNumber等等。

在必要时,JS会自动帮我们把字面量基础类型转为一个对象。

const str = 'im a string'
console.log(str.length) // 11
console.log(str.charAt(3)) // a

这里就自动把string类型的str自动转变为String对象了。所以我们一般定义基础类型并不需要使用对应的内置函数进行定义,直接字面量定义就好啦。null和undefined只有字面量,没有对应的内置对象。Date只有内置对象而没有字面量定义。

内容

对象的内容是存储在特定命名位置的值组成的,我们称之为属性,形式是key-value。在引擎内部,这些值的存储方式是多种多样的,并不会存储在对象容器内部,而是存储对应的引用,这些引用会指向真正的存储位置。想访问对象属性的值,有两种方式,一种是.操作符(属性访问)另一种是[](键访问),这两种的区别在于.操作符只接收任意UTF-8/Unicode字符串作为属性名,像"hello-world"这种属性只能通过["hello-wolrd"]这种方式进行访问。且键访问是可以构造的,举个栗子

const obj = {
    hello: 'world'
}
const a = 'hello'
console.log(obj[a]) // world

属性名永远是字符串,其他类型的会被转为字符串显示。

数组

数组也是通过[]进行访问,虽然[]不限制值的类型,但是数组的期望是数值下标。

const arr = ['hello', 'thank you']
arr[0] // hello
arr[1] // thank
arr.length // 2

复制对象

js没有提供一个官方的方法进行对象的复制,这里不详谈对象的复制问题。只提供一个比较简单的复制思路。

// 浅拷贝
const obj = { a: 1 }
const cp1 = Object.assign({}, obj)
const cp2 = {...obj}

// 深拷贝,在保证对象是安全的json对象时,可以直接这样
const deepCopy = JSON.parse(JSON.stringify(obj))

属性描述符

ES5提供了检测属性特性的方法

const obj = {
    a: 2
}
Object.getOwnPropertyDescriptor(obj, 'a')
/*
{
    configurable: true,
    enumerable: true,
    value: 1,
    writable: true
}
*/

这个属性描述符除了这个属性的value值之外,还有configurableenumerablewritable分别代表可配置、可枚举和可写属性。同时提供了Object.defineProperty和Object.defineProperties用来改变这些配置,这两个函数用途是一致的,只不过一个是改变一个对象里的单个属性,一个是批量修改。

const obj = {
    a: 'hello',
}

Object.defineProperty(obj, 'a', {
    writable: false,
    configurable: false,
    enumerable: false,
    value: 5,
})

console.log(obj) // {a: 5}

  • writable: 这个值如果设为false,那么就不允许修改这个值的,修改的话会报错。
        // 接上面这个例子
        obj.a = 777
        console.log(obj.a) // 5
    
  • configurable:这个设置为false的话就不可以再配置了,所以这个是一个单向的配置,修改了之后无法再进行修改,并且这个属性也不可以被删除了。
        Object.defineProperty(obj, 'a', {
            configurable: true,
        }) // Uncaught TypeError: Cannot redefine property: a
    
  • enumerable: 这个设置了false就不会被遍历出来。
        obj.b = 7
        for (let i in obj) {
            console.log(i, obj[i])
        }
        // 只输出 b, 7
    

getter和setter

对象有隐藏的两个函数 get和set

const obj = {
    get a() {
        return this._a
    },
    set a(val) {
        this._a = val * 2
    }
}
obj.a = 2
console.log(obj.a) // 4

这两个方法也是可以被Object.defineProperty进行劫持,vue2.x就是使用这个方法对对象进行数据劫持的,有兴趣的同学可以自己去了解一下。

const obj = {
    get a() {
        return 2
    },
}

Object.defineProperty(obj, 'b', {
    get() {
        return this.a * 2
    },
})

console.log(obj.a, obj.b) // 2, 4

这里需要注意的一个点就是不可以在get里面调用自身,这样会进入循环引用导致调用栈溢出。

其他

除了defineProperty之外,Object对象还提供了诸如preventExtensionssealfreeze等方法去改变对象的某些默认行为,这里不展开详谈,有兴趣的小伙伴可以自行了解,因为实在是用的不多。

遍历

ES6数组提供了多个遍历方法,forEach单纯遍历,map遍历并返回一个新数组,filter遍历过滤返回所需数据,someevery遍历返回布尔值等等。

而遍历对象,现在一般都是使用for...in来进行遍历,i是属性名,但是这个方法会遍历原型链上的属性。

function foo(a, b) {
    this.a = a
    this.b = b
}
foo.prototype.c = 3
const obj = new foo(1, 2)
for (let i in obj) {
    console.log(obj[i])
}
// 1,2,3

如果不想遍历原型链上的属性,可以通过hasOwnProperty判断过滤原型链上的属性。

function foo(a, b) {
    this.a = a
    this.b = b
}
foo.prototype.c = 3
const obj = new foo(1, 2)
for (let i in obj) {
    if (obj.hasOwnProperty(i)) {
        console.log(obj[i])
    }
}
// 1,2

我们都知道了for..in循环获取到的是key,ES6还提供了for...of语法用来直接遍历值,这需要对象实现迭代器。比如数组本身实现了迭代器对象。

const arr = ['hello', 'thank you']
for (let i of arr) {
    console.log(i)
}
// hello, thank you

对象没有默认实现迭代器,但是我们可以自己来实现一个迭代器,我们可以通过Symbol.iterator来获取对象的迭代器对象@@iterator,然后通过ES6提供的生成器函数进行遍历

const obj = {
    a: 1,
    b: 2,
    [Symbol.iterator]: function* name() {
        const values = Object.values(this)
        for (let i = 0; i < values.length; i++) {
            yield values[i]
        }
    },
}

for (let i of obj) {
    console.log(i)
}
// 1,2

或者可以更直接点,Object.keysObject.valuesObject.entries返回的本身就是一个迭代器,我们可以通过for...of遍历

const obj = {
    a: 1,
    b: 2,
}
for (let i of Object.values(obj)) {
    console.log(i)
}
// 1,2
for (let i of Object.keys(obj)) {
    console.log(i)
}
// a, b
for (let [key, value] of Object.entries(obj)) {
    console.log(key, value)
}
// a 1, b 2

类和JavaScript中的"类"

这章是关于编程语言里类的概念和JavaScript里的类的概念,偏理论的东西,而且书里面也总结的挺好的了,我就不在这里误人子弟了哈哈哈。

原型

说在前面,以下内容都不考虑Proxy,如果考虑Proxy的话有部分不适用。

prototype

JavaScript对象中有个特殊的[Prototype]属性,几乎所有的对象在创建时就有这个属性。当我们想要去引用有个对象时,会触发[Get]操作,对于这个[Get]操作第一步会检查对象本身是否有这个属性,如果有就使用它,如果没有,那么就会继续访问Prototype链,也就是我们口中常说的原型链。

const anotherObj = {
    a: 1,
}
const obj = Object.create(anotherObj)

console.log(obj.a) // 1
console.log(obj.__proto__ === anotherObj) // true

obj是没有a这个属性的,但是我们仍然访问到了它。假如我们访问对象的一个属性,如果我们在源对象找不到这个属性,就会不停的往原型链上去查找这个属性,直到原型链的尽头如果依然不存在,则返回undefined。

Object.prototype

那么原型链的尽头是哪里?答案就是Object.prototype,所有普通的对象的原型链最终都会指向Object.prototype,由于很多对象都指向Object.prototype,所以它包含了许多通用的功能,比如toStringvalueOfhasOwnProperty等。

属性的设置和屏蔽

给对象设置一个属性并没有像获取那样简单。打个比方,foo属性不直接存在于obj这个对象,而是存在于obj对象原型链上的某个对象,当我们执行obj.foo = 'demaxiya~~~'时,将会出现三种情况。

  1. 如果foo出现在原型链上层而且没有标记为只读,那么会直接在原对象obj上添加一个叫foo的属性并赋值为demaxiya~~~,并且会屏蔽掉上层的foo属性,因为[Get]只会取最底层的属性。
  2. 如果foo出现在上层并且标记为只读,那么在严格模式下会报错,普通模式下则会忽略这个修改。
  3. 如果foo在上层并且是一个setter,那么一定会调用这个setter,而且不会添加到obj这个属性上,也不会改变这个setter。

PS:需要注意遇上隐式屏蔽的情况。

const anotherObj = {
    a: 1,
}
const obj = Object.create(anotherObj)

console.log(anotherObj.a) // 1
console.log(obj.a) // 1

obj.a++

console.log(anotherObj.a) // 1
console.log(obj.a) // 2

这里的obj.a++相当于obj.a = obj.a + 1,我们通过原型链上找到a的值为1,然后给这个值加1然后赋值到obj上,也就是触发了上面第一种情况,我们在obj这个对象上添加了一个a属性,并且把原型链上的a属性屏蔽掉了。

JavaScript在不断的去模仿“类”这个概念。在面向对象的语言当中(本书作者称之为面向类的语言,并认为JavaScript才是真正面向对象的语言,因为JS不需要通过类,直接创建对象)通过一个类去实例化多个实例出来。JavaScript没有这个机制,所以尽管我们也可以用new去创建一个或者多个对象,但是这些对象之间并没有完全隔绝,还是会产生部分关联。比如我们通过new Foo()去创建的每一个对象都会通过原型链去链接到Foo.prototype这个原型对象里,那么每个通过Foo创建的对象都会通过这个原型对象进行关联并产生影响。举个栗子

function Foo(a) {
    this.a = a
}

Foo.prototype.b = 'hahaha'

const obj1 = new Foo(1)
const obj2 = new Foo(2)

Foo.prototype.b = 'dadada'

console.log(obj1.b) // dadada
console.log(obj2.b) // dadada

构造函数

Foo.prototype有一个公有且不可枚举的属性constructor,这个属性指向对象关联的函数,同时,由这个函数创建的对象也有constructor这个属性,同样的指向创建对象的函数。

function Foo(a) {
    this.a = a
}

console.log(Foo.prototype.constructor === Foo) // true

const obj = new Foo(1)

console.log(obj.constructor === Foo) // true

其实JS中所谓的构造函数本质上还是函数,只不过通过new调用时会构造一个对象并赋值。 需要注意的一点是constructor这个属性只是一种默认的委托,当你用其他对象去替换掉默认的prototype对象,新对象是不会自动获取constructor属性的,所以再次获取这个constructor属性时,它会往Foo的原型链继续往上找,直到内置的Object对象,这个对象有constructor属性。

function Foo(a) {
    this.a = a
}
Foo.prototype = {
    hello: 'world',
}

console.log(Foo.prototype.constructor === Foo) // false
console.log(Foo.prototype.constructor === Object) // true

如果想constructor依然指向相应的对象,可以通过手动赋值完成,并且需要设置为不可枚举。 所以可以看到constructor属性并不是一个可靠的属性,慎用。

继承

书里面的就不一一列举了,直接上干货~

  1. 原型链继承

    function Parent() {
        this.name = 'hello'
    }
    
    // 原型继承
    function Child1(age) {
        this.age = age
    }
    
    Child1.prototype = new Parent()
    
    const child1 = new Child1(18)
    console.log(child1.age, child1.name) // 18, hello
    

    直接把子类的prototype属性指向父类的实例,这样就可以访问父类的属性和方法。

    缺点:无法向父类构造函数传参;且所有实例共享父类实例的属性,若父类共有属性为引用类型,一个子类实例更改父类构造函数共有属性时会导致继承的共有属性发生变化。

    function Parent() {
        this.name = 'hello'
        this.arr = []
    }
    
    function Child1(age) {
        this.age = age
    }
    
    Child1.prototype = new Parent()
    
    const child1 = new Child1(18)
    const child2 = new Child1(19)
    
    child1.arr.push(666)
    
    console.log(child2.arr) // [666]
    
  2. 构造函数继承

    function Parent() {
        this.name = 'hello'
        this.arr = []
    }
    
    Parent.prototype.sayHi = function () {
        console.log('hi')
    }
    
    function Child2(age) {
        Parent.call(this)
        this.age = age
    }
    
    const child2 = new Child2(18)
    const child3 = new Child2(19)
    console.log(child2.age, child2.name)
    child3.arr.push(666)
    console.log(child2.arr) // []
    child2.sayHi() //  TypeError: child2.sayHi is not a function
    

    通过call改变父类的作用域,将其绑定在子类里,让子类可以访问父类的属性.

    缺点:无法访问父类原型链上的属性

  3. 组合继承

    function Parent() {
        this.name = 'hello'
        this.arr = []
    }
    
    Parent.prototype.say = function () {
        console.log('parent')
    }
    
    function Child3(age) {
        Parent.call(this)
        this.age = age
    }
    
    Child3.prototype = new Parent()
    
    const child3 = new Child3(18)
    console.log(child3.age, child3.name) // 18, hello
    
    child3.say() // parent
    

    组合继承就是综合了以上两种继承。解决了无法继承原型链和复杂对象公用的问题。

    缺点:实例化两次父类函数,造成多余消耗。

  4. 寄生继承

    function clone(obj) {
        const instance = Object.create(obj)
        instance.say = function () {
            console.log('666')
        }
        return instance
    }
    const parent = {
        hello: 'world',
    }
    const child = clone(parent)
    console.log(child.hello) // world
    child.say() // 666
    

    相当于克隆了一个对象,然后通过给这个对象添加方法进行扩展。

    缺点:和原型链继承一样,一个子类实例更改父类构造函数共有属性时会导致继承的共有属性发生变化。

  5. 终极大法:寄生组合继承

    function Father() {
        this.name = '我是你爹'
    }
    
    Father.prototype.hello = function () {
        console.log('im your baba')
    }
    
    function Erzha() {
        this.age = 18
    }
    
    function extend(subClass, superClass) {
        const prototype = Object.create(superClass.prototype) // 创建父类prototype属性的拷贝
        prototype.constructor = subClass // prototype.constructor依然指向父类,这里修改为子类,为啥这么干,请回头看看上面构造函数这一节
        subClass.prototype = prototype // 将拷贝引用到子类的原型上
    }
    
    extend(Erzha, Father)
    Erzha.prototype.say = function () {
        console.log('dadada')
    }
    const test = new Erzha()
    
    console.log(test, test.name, test.age) // Erzha {}, 我是你爹, 18
    test.say() // dadada
    test.hello() // im your baba
    

    这个继承弥补了组合继承的缺点,无需两次构造父类。

检查类关系

我们可以通过几种方式来判断一个实例的继承祖先呢(通常称之为内省或反射)?

  1. instanceof: 我们可以通过instanceof来判断

    function extend(subClass, superClass) {
        const prototype = Object.create(superClass.prototype)
        console.log(prototype.constructor)
        prototype.constructor = subClass
        subClass.prototype = prototype
    }
    
    function Father() {
        this.name = '我是你爹'
    }
    
    Father.prototype.hello = function () {
        console.log('im your baba')
    }
    
    function Erzha() {
        Father.call(this)
        this.age = 18
    }
    
    extend(Erzha, Father)
    Erzha.prototype.say = function () {
        console.log('wodiaonimade')
    }
    const test = new Erzha()
    
    console.log(test instanceof Erzha, test instanceof Father) // true, true
    
  2. isPrototypeOf

    续上面的例子

    console.log(
        Erzha.prototype.isPrototypeOf(test),
        Father.prototype.isPrototypeOf(test)
    ) // true, true
    

isPrototypeOf方法用于测试一个对象是否存在于另一个对象的原型链上。

  1. 我们也可以通过getPrototypeOf获取到直接创建这个对象的函数的原型链

    console.log(Object.getPrototypeOf(test) === Erzha.prototype) // true
    

    绝大多数浏览器都提供了一个__proto__属性来访问Prototype属性,跟getPrototypeOf是一致的

    console.log(test.__proto__ === Erzha.prototype)
    

    但是犹如constructor属性一样,这也不是一个可靠的属性,是可以直接更改的。

    提一嘴我遇到过的面试题

    function Foo() {
        this.name = 'cpx'
    }
    
    const foo = new Foo()
    
    console.log(Foo.prototype, Foo.__proto__)
    console.log(foo.prototype, foo.__proto__)
    

    大家可以思考一下这里会输出啥。

总结

还有一章是关于行为委托的,比较偏向于理论性。建议如果感兴趣的可以搜来看看。暂时先挖个坑吧,后续看看能不能也总结一下。那么你不知道的JavaScript上册也算是看完了(三刷了...),你不知道系列都挺不错的,建议大家入手看看,强力安利~~~ 谢谢你看完摸摸哒