ES5、ES6实现继承,原型及原型链理解
- ES5实现继承
- 对象和函数的原型
- 对象的原型
- 函数的原型
- new、constructor
- 函数原型上的属性
- 优化通过构造函数创建对象
- 原型链
- 原型链实现的继承
- 借用构造函数继承
- 寄生组合实现继承
ES5实现继承
对象和函数的原型
对象的原型
JavaScript当中每个对象都有一个特殊的内置属性[[prototype]]
,这个特殊的对象可以指向另外一个对象。
- 当我们通过引用对象的
属性key来获取一个value时
,它会触发[[Get]]
的操作;- 这个操作会
首先检查该对象是否有对应的属性
,如果有的话就使用它;如果对象中没有改属性,那么会访问对象[[prototype]内置属性指向的对象上的属性
;
那么如果通过字面量直接创建一个对象,这个对象也会有这样的属性吗?如果有,应该如何获取这个属性呢?
答案是有的,只要是对象都会有这样的一个内置属性;
获取的方式有两种:
- 方式一:通过对象的_proto_属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);
- 方式二:通过Object.getPrototypeOf方法可以获取到;
<script>
var obj={name: "kkk",
age:19
}
console.log(obj)
//获取对象的原型
console.log(obj.name,obj.age)console.log(obj._proto_)
console.log(object.getPrototypeOf(obj))
console.log(obj_proto__=== Object.getPrototypeof(obj))// true
//这个原型有什么用呢?
//当我们通过[[get]]方式获取一个属性对应的value时
//1>它会优先在自己的对象中查找,如果找到直接返回
//2>如果没有找到,那么会在原型对象中查找
console.log(obj.name)
console.log(obj.message)//null
//测试自身对象中没有时是否在原型中寻找
obj._proto_.message ="Hello-world"
console.log(obj.message)//Hello-world
</script>
函数的原型
所有的函数都有一个prototype的属性
,(不是_proto_
);对象中是没有这个prototype这个属性的,注意不要和上文中的[[prototype]]
混淆
- 当然,把函数看成是一个普通的对象时,它是具备_proto_(隐式原型),
- 可使用函数名.prototype获取
那么函数的原型有什么作用呢?继续往下看就知道了
new、constructor
之前的笔记中写过new关键字
相关知识(点击回顾):
- 在内存中创建一个新的对象(空对象);
- 这个对象内部的I[[prototype]]属性会被赋值为该构造函数的prototype属性;
那么也就意味着我们通过Person构造函数创建出来的所有对象的[prototype]]属性都指向Person.prototype:
代码验证(new操作原型赋值):
function Foo() {
}
var f1 = new Foo()
var f2 = new Foo()
console.log(f1.__proto__ === f2.__proto__) // true
console.log(f1.__proto__ === Foo.prototype) // true
将方法放在原型上:
function Foo() {
}
var f1 = new Foo()
var f2 = new Foo()
f1.__proto__.name = 'xt'
// Foo.prototype.name = 'xt'
console.log(f1.name) // xt
console.log(f2.name) // xt
在看上方的代码,当在f1.__proto__上面添加了name 属性后,f2.name也是可以打印出相同结果。
这说明了f1.proto 与 Foo.prototype与f2.__proto__他们的内存地址是相等的;而查找的规则就是先在自己里面找,找不到在沿着原型链去找。
函数原型上的属性
函数的prototype都用是有constructor
属性的,用node执行时看不见的原因是enumerable默认为fasle,向这里我们改成了true,就可以看见了。
function Foo() {
}
Object.defineProperty(Foo.prototype, "constructor", {
enumerable: true,
configurable: true
})
// prototype.constructor 指向构造函数本身
console.log(Foo.prototype.constructor); // Foo;/如果foo.prototype,直接打印是空的,但是它不是空的。这里因为我们预选将可枚举的属性设置为ture所以可以看见
console.log(Foo.prototype.constructor.prototype.constructor); // Foo
(构造函数创建对象的内存表现)
constructor保存的是一个地址,这个地址指向构造函数本身。即函数中有prototype属性,也就是函数的原型对象,而这个原型中是有个constructor属性的,并且他的constructor属性的值是函数本身。
优化通过构造函数创建对象
通过构造函数创建对象是创建对象方法之一
function CreateObj(name, age) {
this.name = name
this.age = age
this.getName = function() {
console.log(this.name);
}
}
var obj1 = new CreateObj('kkk', 19)
var obj2 = new CreateObj('kkk', 20)
console.log(obj1);
console.log(obj2);
但这种方法是有弊端的,比如getName 这个函数,在每次new CreateObj时他都会被创建,我们如果不想它每次被创建,就可以通过我们函数的原型来解决,如下:
function CreateObj(name, age) {
this.name = name
this.age = age
}
CreateObj.prototype.getName = function() {
console.log(this.name);
}
var obj1 = new CreateObj('kkk', 18)
obj1.getName() // kkk
var obj2 = new CreateObj('kkk', 20)
obj2.getName() // kkk
原型链
var obj = {
name: 'kkk'
}
obj.__proto__ = {}
obj.__proto__.__proto__ = {}
obj.__proto__.__proto__.__proto__ = {
age: 18
}
console.log(obj.age); // 18
上面的代码,在obj中是没有age属性的,但是当我们在其__proto__上添加了age后他是可以找到的,说明他的查找,是一层一层的在查找。如下图:
(原型链的查找顺序)
我们知道了其查找规则,但是它会不会一直查找呢,答案是不会,因为它是有顶层的,为什么我们能确定他是有顶层的呢?
var obj = {
name: 'kkk'
}
console.log(obj.age); // undefined
因为当obj 中没age属性时,我们直接打印obj.age,他是能马上给我们返回undefined的,那他的顶层又是什么呢?
var obj = {
name: 'kkk'
}
console.log(obj.__proto__); // [Object: null prototype] {}
[Object: null prototype] {} 这个就是我们的顶层,这个是在node环境下的打印,看着不太明显,我们可以在浏览器中执行(浏览器中他为了我们方便调试,是有帮我们特殊处理的,所以我们会更容易看懂);这里的__proto__是null,其实我们对象原型的顶层,就是null
看了对象的,我们再来看函数的
function foo(params) {
}
console.log(foo.prototype.__proto__.__proto__);// null
我们会发现看到这里依旧是null,同时我们也应该知道函数的prototype属性值中,也是有__proto__的。当然,我们还可以知道,这里的foo其实是继承Object的,所以原型链最顶层的原型对象就是Object的原型对象
原型链实现的继承
继承的主要目的,就是为了减少重复代码。
当我们学完了,原型链其实我们就可以通过原型链来自己实现继承。
// 父类
function Person() {
this.name = 'kkk'
}
Person.prototype.eating = function() {
console.log(this.name + 'eating');
}
// 子类
function Student() {
this.sno = 1
}
// 通过原型继承
Student.prototype = new Person()
Student.prototype.studying = function() {
console.log(this.name + 'studying');
}
var stu = new Student()
console.log(stu.name); // kkk
通过这种方法实现继承的弊端:
- 当我们打印stu的时候(node中),我们是看不到name这个属性的,因为name在person中。
- 如果是引用类型,可能会有传值引用问题。
- 不能给Person传递参数
解决方法:借用构造函数继承
借用构造函数继承
function Person(name) {
this.name = 'xt'
}
...
Student.prototype.studying = function() {
Person.call(this, name)
console.log(this.name + 'studying');
}
...
通过构造函数继承,我们前面的弊端解决了,但同时又产生了其他弊端,比如:
- Person至少要被调两次
- stu原型对象会多出一些属性,但是这些属性时没有存在的必要的
解决方法:寄生组合继承
寄生组合实现继承
// 父类
function Person(name) {
this.name = name
}
Person.prototype.eating = function() {
console.log(this.name + 'eating');
}
// 子类
function Student(name, sno) {
Person.call(this, name)
this.sno = sno
}
// 寄生式核心代码
function inheritPrototype(subType, superType) {
subType.prototype = Object.create(superType.prototype)
subType.prototype.constructor = subType
}
inheritPrototype(Student, Person)
Student.prototype.studying = function() {
console.log(this.name + 'studying');
}
var stu = new Student('kkk', 1)
console.log(stu); // Student { name: 'kkk', sno: 1 }