# 原型和原型链
# 对象的原型
JavaScript中每个对象都有一个特殊的内置属性[[prototype]],这个属性可以称之为对象的原型(隐式原型),这个特殊的对象可以指向另外一个对象。
早期的ECMA没有规范如何查看[[prototype]],浏览器提供了一个
__proto__
属性便于查看。
var obj = { name: "okarin" }
console.log(obj.__proto__)//{}
2
3
4
ES5推出了Object.getPrototypeOf() 获取原型 方法可以查看[[prototype]]
var obj = { name: "okarin" }
console.log(Object.getPrototypeOf(obj))//{}
2
3
# 对象的原型的作用
当我们从一个对象中获取某一个属性是,它会触发[[get]]操作。
- 在当前对象中去查找对应的属性,如果找到就直接使用
- 如果在
__proto__
没有找到,就会沿着它的原型链去查找
# 函数的原型
函数作为对象来说,也是有[[prototype]]隐式原型。
function foo(){
}
console.log(foo.__proto__)//{}
2
3
4
5
6
因为函数是一个函数,所以会多出来一个显示原型属性:prototype
function foo(){
}
console.log(foo.prototype)//foo {}
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
// }
// }
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
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
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
})
2
3
4
5
6
7
8
9
10
# 原型链的理解
当获取对象中的属性时,会通过[[get]]操作,在当前的对象中查找属性,如果没有找到,这个时候会去沿着原型链__proto__
对象上一层一层查找,直到找到顶层。
如果找到顶层都没有找到就会返回undefined
# 顶层原型
# 构造函数原型中的__proto__
function Person(){
}
console.log(Person.prototype.__proto__)//{}
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~
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)
2
3
- 会在 stu 对象里面的属性查找,查找不到之后会在 stu对象的
__proto__
隐式原型里面查找,stu对象的__proto__
指向Student 的默认原型对象。 - 在 Student 的默认原型对象里面的属性里查找,查找不到后会在 Student 的默认原型对象的
__proto__
隐式原型里面查找,Student 的默认原型对象的__proto__
指向顶层对象。 - 然后在顶层对象的属性里面进行查找,查找不到会在顶层对象的
__proto__
隐式原型里面查找,顶层对象的__proto__
指向的是null。
所以结果是 undefined 和 未定义
console.log(stu.name)//undefined
console.log(stu.eating)//undefined
2
而当我们在创建Student类之后(这时还没有通过new创建stu对象),执行:
p = new Person()
Student.prototype = p
Student.prototype.studying = function(){
console.log(this.name + "在studying~")
}
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 时:
- 会在 stu 对象里面的属性查找,查找不到之后会在 stu对象的
__proto__
隐式原型里面查找,这时stu对象的__proto__
指向的是p对象。 - 在 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~
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)
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 }
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 }
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 }
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()
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 }
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
}
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 }
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 }
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