原型、原型链、继承
这是一个炒冷饭的话题,既然是很老旧的知识点,那还有这么多人去炒,肯定是说明这其中有事儿:
- 有难以揣摩的、
- 不好记忆的、
- 容易混淆的;
那这此就一起给收拾干净!
开局代码
function Person(name) {
this.name = name;
}
Person.prototype.age = 18;
var person = new Person();
上面的代码咱们来看两个打印:
console.dir(person);
console.log(typeof person, typeof Person, typeof Person.prototype);
原型
在JavaScript中每个对象(除null外)创建的时候,就会关联另一个对象,这个关联对象就是原型对象,与大多数资料一致,后续简称原型(本质也是对象),每一个对象都会从原型中继承属性。
1) prototype
从这里可以看到,每个函数都有一个prototype属性,这个属性指向的是函数的原型对象。函数的原型可以通过.prototype属性获取到,可以读取或设置其原型对象的属性。
2) __ proto__
console.log(person.__proto__ === Person.prototype); // true
每个对象(除null外)都会有一个__proto__属性,这个属性可以获取到该对象的原型
3) constructor
console.log(Person === Person.prototype.constructor); //true
每个原型都有constructor属性,指向关联它的构造函数。
4) instanceof
有了以上的基础,我们继续探讨,怎么知道Person是不是person的原型上的构造函数呢? instanceof就是处理这个事情,如果A沿着原型链能找到B.prototype,那么A instanceof B为true,此时B却不一定是A直接new出来的,切记,看几个特例;
console.log(Object instanceof Object);//true
console.log(Function instanceof Function);//true
console.log(Function instanceof Object);//true
console.log(Object instanceof Function);//true
console.log(Number instanceof Number);//false
console.log(String instanceof String);//false
console.log(Person instanceof Function);//true
console.log(Person instanceof Person);//false
原型链
1) 套娃
原型是一个对象,原型的原型是什么?
我们可以反复去打印获悉:
console.log(person.__proto__.__proto__) //Object
console.log(person.__proto__.__proto__.__proto__); //null
于是,用一张图展示出person的原型链,那应该是这样:
2) 澄清概念
person套娃链结束,然而真正的难点却才刚刚开始,如果认真看到这里的话:
- 上面的instanceof特例是怎么回事?
- Object和Function为何搞特殊待遇?
不急,我们先来澄清一些概念,至少到目前为止可以帮助总结:
那么,我们归纳和精简一下:
谈到这里,不得不上一张永恒的图,Js灵魂图:(对与老前端来讲,那是曾经夕阳下的奔跑...磕磕~)
3) 特例单独记忆(易混淆)
Object.__proto__ === Function.prototype;
Function.prototype.__proto__ === Object.prototype;
Object.prototype.__proto__ === null;
这里推荐一个有趣的故事记忆法则,偶然发现的,觉得有意思它就有意思,觉得无聊就跳过此处:
继承
现在扩充一下父类Person,使其具有以下属性和方法:
function Person(name) {
this.name = name || "noName";
this.sleep = function () {
console.log(this.name + "好想睡觉");
};
}
Person.prototype.eat = function (food) {
console.log(this.name + "想吃" + food);
};
1) 原型链继承
// 原型链继承
function Man(){}
Man.prototype = new Person();
let man = new Man();
- 优点:
- 这样将父类的示例作为子类的原型,容易实现。
- 父类后续新增的属性或方法,子类中都可以访问。
- 操作子类即可修改原型中的属性。
- 缺点:
- 要新增原型中属性或方法,必须要先new一个实例。
- 无法多继承
- 创建子类实例时,无法向父类构造函数传参
2) 构造函数继承
// 构造函数继承
function Man(name) {
Person.call(this, name);
this.name = name || "No Name";
}
let man = new Man("jack");
- 优点:
- 解决了原型链继承中不可传递参数的缺点。
- 子类可以继承多个父类,使用多个call或apply之类的
- 缺点:
- 这里的man是Man的实例,但不是Person的实例
- 父类Person原型中的属性和方法(eat)无法被继承。
- 子类实例man虽然可以继承父类Person的属性和方法,但是父类Person中的函数无法被复用,如上代码中的sleep()
3) 实例继承
// 实例继承
function Woman(name) {
let instance = new Person();
instance.name = name || "no name";
return instance;
}
var women = new Woman();
- 优点:
- 不限制调用方式,无论是new Woman()还是Woman()都可以实现继承
- 子类可以继承多个父类,使用多个call或apply之类的
- 缺点:
- woman是Person的实例,而不是Woman的实例
- 不支持多继承
4) 组合继承
//组合继承
function Man(name) {
Person.call(this, name);
this.name = name || "no name";
}
// 将整个原型链规整
Man.prototype = new Person();
Man.prototype.constructor = Man;
let man = new Man();
- 优点:
- man实例既能获取Man的属性和方法,也能获取到Person的原型对象的属性和方法(eat)
- man即是子类Man的实例,也是父类Person的实例
- 弥补了单纯的构造继承的缺点,可以向父类Person传值
- 可以复用所有对象的函数
- 缺点:
- 调用了两次父类构造函数,生成了两份实例,多了一些些内存消耗。
5) 寄生组合继承
//寄生组合继承
function Woman(name) {
Person.call(this, name);
this.name = name || "No Name";
}
(function () {
// Super无实例方法,避免了上一种继承方式的创建两次实例
var Super = function () {};
Super.prototype = Person.prototype;
Woman.prototype = new Super();
});
Woman.prototype.constructor = Woman;
let woman = new Woman();
- 优点:
- 在组合继承方式优点上,解决了创建两次实例的问题
- 缺点:
- 实现复杂
6) ES6 Class继承
Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。
// Class 继承
class A {}
class B extends A {}
B.__proto__ === A; // true
B.prototype.__proto__ === A.prototype; // true
再看:
//继承
class A extends Object {}
A.__proto__ === Object; // true
A.prototype.__proto__ === Object.prototype; // true
//非继承
class A {}
A.__proto__ === Function.prototype; // true
A.prototype.__proto__ === Object.prototype; // true
A.prototype.__proto__ === Function.prototype; // false
ES5继承与ES6继承的区别
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。
ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
class 的职责是充当创建 object 的模板, 通常来说,data 数据是由 instance 承载,而 methods 行为/方法则在 class 里。
// 特殊 如果子类没有定义constructor方法,这个方法会被默认添加,
class ColorPoint extends Point {
}
// 等同于
class ColorPoint extends Point {
constructor(...args) {
super(...args);
}
}
new 浅析
1) 做了什么?拆解:
2) 实现new方法
- 调用需求:createInstance(Person, {name: 'Tom', age:20});
- 实现思路:
- 创建一个空对象
- 从参数中删除第一个元素并返回,第一个参数(就是构造函数),剩下就是参数
- 链接到原型
- 调用构造函数,把this绑定到新对象上
- 返回构造函数调用的结果,或者新对象
- 代码实现一:
function createInstance() {
let obj = {}
let constructor = [].shift.call(arguments)
obj.__proto__ = constructor.prototype
let result = constructor.apply(obj, arguments)
return typeof result === 'object' ? result : obj
}
- 代码实现二:
const createInstance = (Constructor, ...args) => {
let instance = Object.create(Constructor.prototype);
Constructor.call(instance, ...args);
return instance;
};
function User(firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
}
小结
一开始的结构序幕拉得太大了,导致这篇慢慢倒腾,居然篇幅这么长,后面有时间看看是否需要调整一下内容板块,拆两篇还是重新整合,暂时就这样了,虽然长一些,但结构还算清晰!
文中如有错误,欢迎在评论区指正,如果有所帮助,欢迎点赞和关注!
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!