JavaScript原型链和继承

2021/12/14 JavaScript原型继承

# 原型和原型链

# 对象的原型

JavaScript中每个对象都有一个特殊的内置属性[[prototype]],这个属性可以称之为对象的原型(隐式原型),这个特殊的对象可以指向另外一个对象

早期的ECMA没有规范如何查看[[prototype]],浏览器提供了一个__proto__属性便于查看。

var obj = { name: "okarin" }

console.log(obj.__proto__)//{}

1
2
3
4

ES5推出了Object.getPrototypeOf() 获取原型 方法可以查看[[prototype]]

var obj = { name: "okarin" }

console.log(Object.getPrototypeOf(obj))//{}
1
2
3

# 对象的原型的作用

当我们从一个对象中获取某一个属性是,它会触发[[get]]操作。

  1. 在当前对象中去查找对应的属性,如果找到就直接使用
  2. 如果在__proto__没有找到,就会沿着它的原型链去查找

# 函数的原型

函数作为对象来说,也是有[[prototype]]隐式原型

function foo(){

}

console.log(foo.__proto__)//{}

1
2
3
4
5
6

因为函数是一个函数,所以会多出来一个显示原型属性:prototype

function foo(){

}

console.log(foo.prototype)//foo {}

1
2
3
4
5
6

函数的显示原型 prototype 会指向函数默认的原型对象

这个对象包括 constructor 在内的很多属性,通过构造函数创建的对象,对象中的__proto__会指向函数的prototype

# 函数原型的属性

# constructor属性
function foo(){

}

console.log(Object.getOwnPropertyDescriptors(foo.prototype))

// {
//     constructor: {
//       value: [Function: foo],
//       writable: true,        
//       enumerable: false,
//       configurable: true
//     }
//   }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

prototype.constructor 指向构造函数本身

# 给prototype添加属性

function foo(){

}

foo.prototype.name = "okarin"

var  f1 = new foo()
console.log(f1.name)//okarin
1
2
3
4
5
6
7
8
# 直接修改整个prototype对象
function foo(){

}

foo.prototype = {
    constructor:foo,
    name:"okarin"
}

var  f1 = new foo()
console.log(f1.name)//okarin

1
2
3
4
5
6
7
8
9
10
11
12
# 通过Object.defineProperty方式添加constructor
function foo(){

}

Object.defineProperty(foo.prototype,"constructor",{
    enumerable:false,
    configurable:true,
    writable:true,
    value:foo
})
1
2
3
4
5
6
7
8
9
10

# 原型链的理解

当获取对象中的属性时,会通过[[get]]操作,在当前的对象中查找属性,如果没有找到,这个时候会去沿着原型链__proto__对象上一层一层查找,直到找到顶层。

如果找到顶层都没有找到就会返回undefined

去

# 顶层原型

# 构造函数原型中的__proto__

function Person(){
    
}
console.log(Person.prototype.__proto__)//{}
1
2
3
4

构造函数的原型对象也是指向顶层对象的

# JavaScript中的类和对象

# 面向对象的特性

# 面向对象的继承

继承可以将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。

# 为什么要有继承

类与类之间有很多重复属性,所以将公共的东西抽取到父类中。子类继承父类的方法和属性。可以减少代码量。

# 继承的实现

# 通过原型链直接实现继承
function Person(){
    this.name = "why"
}


Person.prototype.eating = function(){
    console.log(this.name + "在eating~")
}

function Student(){
    this.sno = 123
}

Student.prototype = new Person()

Student.prototype.studying = function(){
    console.log(this.name + "在studying~")
}


var stu = new  Student()
console.log(stu.name)//why
stu.eating//why在eating~
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

构造函数 Person 有一个**显示原型 **prototype 指向 Person 默认的原型对象

这个对象有constructor属性。

我们给 Person 的prototype 添加了 eating 属性。

同样的,构造函数 Student 有一个**显示原型 **prototype 指向 Student 默认的原型对象,同时添加了 studying 属性。

如果这时我们通过 new Student 创建了 stu 对象,这时,stu对象的__proto__隐式原型会指向 Student 的默认原型对象。

当我们执行 stu.name 和 stu.eating 时:

var stu = new  Student()
console.log(stu.name)
console.log(stu.eating)
1
2
3
  1. 会在 stu 对象里面的属性查找,查找不到之后会在 stu对象的__proto__隐式原型里面查找,stu对象的__proto__指向Student 的默认原型对象。
  2. 在 Student 的默认原型对象里面的属性里查找,查找不到后会在 Student 的默认原型对象的__proto__隐式原型里面查找,Student 的默认原型对象的__proto__指向顶层对象。
  3. 然后在顶层对象的属性里面进行查找,查找不到会在顶层对象的__proto__隐式原型里面查找,顶层对象的__proto__指向的是null。

所以结果是 undefined 和 未定义

console.log(stu.name)//undefined
console.log(stu.eating)//undefined
1
2

而当我们在创建Student类之后(这时还没有通过new创建stu对象),执行:

p = new Person()
Student.prototype = p
Student.prototype.studying = function(){
    console.log(this.name + "在studying~")
}
1
2
3
4
5

先通过 new Person 创建了 p 对象,这时,p对象的__proto__隐式原型会指向 Person 的默认原型对象。

紧接着我们将Student的显示原型prototype指向p对象

然后我们通过 Student 的prototype 添加了 studying 属性。这时的prototype 指向的是 p 对象。所以在 p 对象上添加了studying属性。

然后我们通过new 创建了 stu对象,这时stu对象的__proto__隐式原型会指向 Student 的 prototype 对象指向的对象(这时候已经指向了 p对象),这时候指向的便是p对象。

这时候我们执行 stu.name 和 stu.eating 时:

  1. 会在 stu 对象里面的属性查找,查找不到之后会在 stu对象的__proto__隐式原型里面查找,这时stu对象的__proto__指向的是p对象。
  2. 在 p对象里面的属性里查找,这时候找到了name属性,而eating属性查找不到后会在 p对象的__proto__隐式原型里面查找,p 的默认原型对象的__proto__指向Person对象,这时候找到了eating属性。

所以结果是why 和 [Function]:

console.log(stu.name)//why
console.log(stu.eating)//[Function]
stu.eating() //why在eating~
1
2
3
# 原型链实现继承的弊端
  • 如果打印对象,继承的属性是看不到的

    在之前的案例中当我们打印stu对象时,看不到其他的属性

    console.log(stu)//Person { sno: 123 }
    
    1
  • 创建两个对象,两个对象中引用的属性会相互影响

    function Person(){
        this.name = "why"
        this.friend = []
    }
    
    Person.prototype.eating = function(){
        console.log(this.name + "在eating~")
    }
    
    function Student(){
        this.sno = 123
    }
    
    Student.prototype = new Person()
    
    Student.prototype.studying = function(){
        console.log(this.name + "在studying~")
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18

    这时获取引用,修改引用中的值,会互相影响

    var stu1 = new  Student()
    var stu2 = new  Student()
    
    stu1.friends.push("张三")
    
    console.log(stu1.friends,stu2.friends)
    //[ '张三' ] [ '张三' ]
    
    1
    2
    3
    4
    5
    6
    7

    而直接修改对象中的属性,时给本对象添加了一个新的属性

    var stu1 = new  Student()
    var stu2 = new  Student()
    
    stu1.name = "张三"
    console.log(stu1.name,stu2.name)
    //张三 why
    console.log(stu1,stu2)
    //Person { sno: 123, name: '张三' } Person { sno: 123 }
    
    1
    2
    3
    4
    5
    6
    7
    8
  • 实现类的过程中不好传递参数

    function Student(name){
        this.name = name
        this.sno = 123
    }
    
    1
    2
    3
    4

    我们本来就是想在父类中处理name的,这样传参的话就违背了这个原则。

# 借用构造函数实现继承

function Person(name,age,friends){
    this.name = name
    this.age = age
    this.friends = friends
}


Person.prototype.eating = function(){
    console.log(this.name + "在eating~")
}

function Student(name,age,friends,sno){
    Person.call(this,name,age,friends)
    this.sno = sno
}

Student.prototype = new Person()

Student.prototype.studying = function(){
    console.log(this.name + "在studying~")
}


var stu = new Student("张三",18,["小明"],1001)

console.log(stu)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

通过Person.call()传入this,将Person中的this修改为当前的this,从而解决之前的弊端。

# 借用构造函数的弊端
  • Person函数至少被调用了两次

  • stu的原型p对象会多出一些属性,没有必要存在

# 原型式继承的实现

var obj = {
    name:"why",
    age:18
}

function createObject(o){
    var newObj = {}
    Object.setPrototypeOf(newObj,o)
    return newObj
}
var info = createObject(obj)

console.log(info)//{}
console.log(info.__proto__)//{ name: 'why', age: 18 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 道格拉斯的方式:
var obj = {
    name:"why",
    age:18
}

function createObject(o){
    function Fn(){}
    Fn.prototype = o
    var newObj = new Fn()
    return newObj
}

var info = createObject(obj)

console.log(info)//{}
console.log(info.__proto__)//{ name: 'why', age: 18 }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

把对象设置为函数的原型,然后new一个函数对象,把这个对象的原型绑定到函数的原型对象,这样就将对象的原型绑定到了父对象的原型上面。

  • 新的方法
var obj = {
    name:"why",
    age:18
}

var info = Object.create(obj)

console.log(info)//{}
console.log(info.__proto__)//{ name: 'why', age: 18 }
1
2
3
4
5
6
7
8
9
# 寄生式继承的实现

var personObj = {
    running:function(){
        console.log("running")
    }
}

function createStudent(name){
    var stu = Object.create(personObj)
    stu.name = name
    stu.studying = function(){
        console.log("studying~")
    }
    return stu
}

var stu1 = createStudent("张三")
var stu2 = createStudent("李四")

console.log(stu1,stu2)

stu1.running()
stu2.running()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 寄生组合式继承
function Person(name,age,friends){
    this.name = name
    this.age = age
    this.friends = friends
}


Person.prototype.eating = function(){
    console.log(this.name + "在eating~")
}

function Student(name,age,friends,sno){
    Person.call(this,name,age,friends)
    this.sno = sno
}

Student.prototype = Object.create(Person.prototype)


Student.prototype.studying = function(){
    console.log(this.name + "在studying~")
}


var stu1 = new Student("张三",18,["小明"],1001)
var stu2 = new Student("李四",19,["小明"],1002)

console.log(stu1)//Person { name: '张三', age: 18, friends: [ '小明' ], sno: 1001 }
console.log(stu2)//Person { name: '李四', age: 19, friends: [ '小明' ], sno: 1002 }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

而当我们在创建Student类之后(这时还没有通过new创建stu对象),执行:

Student.prototype = Object.create(Person.prototype)

//等价于
Student.prototype = createObject(Person.prototype)

function createObject(Person.prototype){
    function Fn(){}
    Fn.prototype = Preson.prototype
    var newObj = new Fn()
    return newObj
}
1
2
3
4
5
6
7
8
9
10
11

这时候会在函数内部把 Person.prototype 指向的默认原型对象 设置为 Fn函数的显示原型prototype指向的原型对象,然后new一个Fn函数对象,这时候newObj对象的__proto__就指向的Fn函数的prototype,也就是指向了Person的prototype这样,然后返回了newObj 赋值给了 Student.prototype,于是Student.prototype就指向了Person.prototype。就将对象的原型绑定到了父对象的原型上面。

# 解决类的名字问题
Student.prototype = Object.create(Person.prototype)
Object.defineProperty(Student.prototype,"constructor",{
    enumerable:false,
    configurable:true,
    writable:true,
    value:Student
})

var stu1 = new Student("张三",18,["小明"],1001)
var stu2 = new Student("李四",19,["小明"],1002)

console.log(stu1)
//Student { name: '张三', age: 18, friends: [ '小明' ], sno: 1001 }

console.log(stu2)
//Student { name: '李四', age: 19, friends: [ '小明' ], sno: 1002 }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

因为当前类的名字是按照当前函数的prototype显示原型 所指向的原型对象constructor 属性 所指向的构造函数 的名字来显示的。而当前student的prototype显示原型是指向 Person 构造函数的prototype显示原型 所指向的原型对象constructor 指向的就是Person构造函数,所以打印的就是Person。而我们通过属性描述符将Student的prototype中的constructor 进行了赋值。

# 寄生组合式继承的优化
function inheritPrototype(SubType,SuperType){
    SubType.prototype = Object.create(SuperType.prototype)
    Object.defineProperty(SubType.prototype,"constructor",{
        enumerable:false,
        configurable:true,
        writable:true,
        value:SubType
    })
}


function Person(name,age,friends){
    this.name = name
    this.age = age
    this.friends = friends
}


Person.prototype.eating = function(){
    console.log(this.name + "在eating~")
}

function Student(name,age,friends,sno){
    Person.call(this,name,age,friends)
    this.sno = sno
}

inheritPrototype(Student,Person)

Student.prototype.studying = function(){
    console.log(this.name + "在studying~")
}


var stu1 = new Student("张三",18,["小明"],1001)
var stu2 = new Student("李四",19,["小明"],1002)

console.log(stu1)
//Student { name: '张三', age: 18, friends: [ '小明' ], sno: 1001 }

console.log(stu2)
//Student { name: '李四', age: 19, friends: [ '小明' ], sno: 1002 }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Last Updated: 2022/2/7下午10:20:47