原型和原型链
# 原型与原型链特别重要
概念
在js
内部使用构造函数来新建一个对象,每一个构造函数里面都有一个prototype
属性,他的值是一个对象,包含了由该构造函数的所有实例共享属性和方法,当使用构造函数新建一个对象时。对象内部将包含指针 指向构造函数的prototype
属性对应的值,获取prototype
可以用Object.getPrototypeOf()
笔记
proto
是隐式原型,一个对象的隐式原型指向构造该对象的构造函数的原型,这也保证了实例能够访问在构造函数原型中定义的属性和方法,ES5
中有了对于这个内置属性标准的Get
方法Object.getPrototypeOf()
.
但是Object.prototype
这个对象是个例外,它的__proto__
值为null
作用是构成原型链,同样用于实现基于原型的继承。举个例子,当我们访问obj
这个对象中的x
属性时,如果在obj
中找不到,那么就会沿着__proto__
依次查找
prototype
是显式原型,每一个函数在创建之后都会拥有一个名为prototype
的属性,这个属性指向函数的原型对象。
但是通过Function.prototype.bind
方法构造出来的函数是个例外,它没有prototype
属性。
作用是用来实现基于原型的继承与属性的共享。
两者的关系是隐式原型指向创建这个对象的函数(constructor
)的prototype
笔记
constructor
是返回创建该对象的函数的引用。
简单来说就是找到这个对象是通过什么构造函数来生成它的,通过constructor
就能找到这个函数
function Person(area) {
this.type = "person";
this.area = area;
}
Person.prototype.sayArea = function () {
console.log(this.area);
};
var Father = function (age) {
this.age = age;
};
Father.prototype = new Person("Beijin");
console.log(Person.prototype.constructor); //function person()
console.log(Father.prototype.constructor); //function person()
Father.prototype.constructor = Father; //修正
console.log(Father.prototype.constructor); //function father()
var one = new Father(25);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
我们可以看出如果是单纯function(){}
出来则它的constructor
是最顶层的Function
,但是假设他是new
出来或者继承了原型的话,他会先找自身的constructor
,找不到的话依次去原型链上找constructor
,这里为了修正,可以指回去,但是假如不指回去的话也有应用,我们写一个插件,别人得到的是你实例化的对象,如果别人想扩展一下对象的话,可以通过instance.prototype.constructor
去继承来的原型上修改或者新增共享的方法
# 原型修改和重写致constructor
丢失
function Person(name) {
this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('Hello')
console.log(p.__proto__ === Person.prototype) // true 最终都指向Object
console.log(p.__proto__ ==== p.constructor.prototype) // true p的constructor是Person 因此为true
// 重写原型
Person.prototype = {
getName: function() {}
}
var p = new Person('Hello')
// p.constructor = Person 这里指回来
console.log(p.__proto__ === Person.prototype) // true 最终都指向Object
console.log(p.__proto__ ==== p.constructor.prototype) // false 给Person的原型对象用对象赋值是,构造函数指向了根构造函数Object 想变成true就得指回来
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 继承的几种方式特别重要
- 原型链继承
function Parent() {
this.name = 'crucials'
}
Parent.prototype.getName = function() {
return this.name;
}
function Child() {}
// 这里也可以直接写出Child.prototype = Parent.prototype
// 但是这样就不能访问到父类的构造函数的属性了,即this.name
Child.prototype = new Parent()
var child = new Child()
child.getName() // crucials
// 优点:逻辑简单
// 父类构造函数中的引用类型(比如对象/数组),会被所有子类实例共享。
// 其中一个子类实例进行修改,会导致所有其他子类实例的这个值都会改变
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- 构造函数继承
function Parent() {
this.name = ['crucials']
}
function Child() {
Parent.call(this)
}
var child = new Child()
child.name.push('fe')
var child2 = new Child() // child2.name === ['crucials']
// 优点:解决了原型链继承中构造函数引用类型共享的问题,同时可以向构造函数传参(通过call传参)
// 缺点:所有方法都定义在构造函数中,每次都需要重新创建(对比原型链继承的方式,方法直接写在原型上,子类创建时不需要重新创建方法)
2
3
4
5
6
7
8
9
10
11
12
13
14
- 组合继承
function Parent() {
this.name = 'crucials'
}
Parent.prototype.getName = function() {
return this.name
}
function Child() {
Parent.call(this)
this.topic = 'fe'
}
Child.prototype = new Parent()
// 需要重新设置子类的constructor,Child.prototype = new Parent()相当于子类的原型对象完全被覆盖了
Child.prototype.constructor = Child
// 优点:同时解决了构造函数引用类型的问题,同时避免了方法会被创建多次的问题
// 缺点:父类构造函数被调用了两次。同时子类实例以及子类原型对象上都会存在name属性。虽然根据原型链机制,并不会访问到原型对象上的同名属性,但总归是不美。
// 具体是Parent.call调用了一次父类构造函数,Child.prototype = new Parent()又调用了一次父类构造函数
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 寄生组合继承
function Parent() {
this.name = 'crucials'
}
Parent.prototype.getName = function () {
return this.name
}
function Child() {
Parent.call(this)
}
Child.prototype = Object.create(Parent.prototype)//核心代码
Child.prototype.constructor = Child//核心代码
var s1 = new Child()
console.log(s1 instanceof Child, s1 instanceof Parent) // true true
console.log(s1.constructor) //Child
// 解决上面全部缺点
// 暂时无缺点
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
问题1传入的为什么是Parent.prototype而不是Parent,问题2为什么Object.create可以不二次调用自身实例上方法
问题一先参考一下Object.create的实现,然后会发现他是创建了一个函数,往函数的原型上面添加传入的对象,假如传入的是Parent
的话他是一个函数,而prototype
是对象,自然会出现奇奇怪怪的错误导致访问不到原型上面的属性,解决了问题一
而问题二Object.create
不会调用父类构造函数是因为他只继承prototype
,并没有调用构造函数,相反new他在改变this
之后还会执行一下构造函数,问题二解决
ES6
class Parent {
constructor() {
super()
this.name = 'crucials'
}
getName() {
return this.name
}
}
class Child extends Parent {
constructor() {
// 这里很重要,如果在this.topic = 'fe'后面调用,会导致this为undefined,具体原因可以详细了解ES6的class相关内容,这里不展开说明
super()
this.topic = 'fe'
}
}
const child = new Child()
child.getName() // crucials
//注意es6的class可以看成是上面栗子中是parent.call(this, value)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 原型链的终点是什么
Object.prototype.__proto__ = null
# 如何获得对象非原型链上的属性
使用obj.hasOwnProperty(key)
# ES5
的继承和ES6
的继承有什么区别?重要
ES5
的继承时通过prototype
或构造函数机制来实现。ES5
的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this
上(Parent.apply(this))
。
ES6
的继承机制完全不同,实质上是先创建父类的实例对象this
(所以必须先调用父类的super()
方法),然后再用子类的构造函数修改this
。
具体的:ES6
通过class
关键字定义类,里面有构造方法,类之间通过extends
关键字实现继承。子类必须在constructor
方法中调用super
方法,否则新建实例报错。因为子类没有自己的this
对象,而是继承了父类的this
对象,然后对其进行加工。如果不调用super
方法,子类得不到this
对象。
值得注意的是
super
关键字指代父类的实例,即父类的this
对象。在子类构造函数中,调用super
后,才可使用this
关键字,否则报错。
# 几道原型链的题
Function.prototype.a = () => console.log(1);
Object.prototype.b = () => console.log(2);
function A () {};
var a = new A();
A.a(); // 1
a.a(); // Uncaught TypeError: a.a is not a function at <anonymous>:1:3
a.b(); // 2
2
3
4
5
6
7
8
A.a()
在A中找,A
里面没有,就根据原型链往上找,A.prototype
上没有,但A
是Function
的实例,Function.prototype
中有a
属性,所以打印1
.new
关键字,除了Function
这个内置的构造器之外构建的实例的类型都是对象;对象a
里没有a
属性,只能往上找,a
的原型链中到Object.prototype
没有a
属性,只有b
属性,所以a.a()
就会报错,a.b()
就会打印2
。
function A() {
B = function() {
console.log(10)
}
return this
}
A.B = function() {
console.log(20)
}
A.prototype.B = function() {
console.log(30)
}
var B = function() {
console.log(40)
}
function B() {
console.log(50)
}
// questions
A.B() // 20
B() // 40
A().B() // 10
B() // 10
new A.B() // 20
new A().B() // 30
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
先不管下面的问题,执行变量提升后 输出40
的已经把50
的给覆盖掉了,下面看题
A.B
执行第二个函数 输出20
B
执行第四个函数 输出40
A()
最后返回this
,因为A
函数在全局作用域,所以this
是window
,执行B
函数,因为A
函数里面的B
没有关键字声明,泄露到全局作用域中,覆盖了之前的B
函数,所以输出10
- 输出
10
new
会匹配离他最近的括号,A.B
是20
那new A.B()
也是20
- 先
new A
在执行里面的B
函数,注意new
之后只会继承他实例上和原型上的属性,不会继承例如A.B = xxx
故输出30