js中的原型与原型链
原型基础
原型对象
每个对象都有一个原型prototype
对象,通过函数创建的对象也将拥有这个原型对象。原型是一个指向对象的指针。
- 可以将原型理解为对象的父亲,对象从原型对象继承来属性
- 原型就是对象除了是某个对象的父母外没有什么特别之处
- 所有函数的原型默认是
Object
的实例,所以可以使用toString/toValues/isPrototypeOf
等方法的原因 - 使用原型对象为多个对象共享属性或方法
- 如果对象本身不存在属性或方法将到原型上查找
- 使用原型可以解决,通过构建函数创建对象时复制多个函数造成的内存占用问题
- 原型包含
constructor
属性,指向构造函数 - 对象包含
__proto__
指向他的原型对象
其中:__proto__(谷歌浏览器已更新为[[prototype]],隐式原型)
我们可以用Object.getPrototypeOf来看他的原型
let x = [];
let y = {};
console.log(Object.getPrototypeOf(x)); //Array(0)
console.log(Object.getPrototypeOf(y)); //Object
下面两个变量的原型都是对象
let x = {};
let y = {};
console.log(Object.getPrototypeOf(x) === Object.getPrototypeOf(y)); //true
我们也可以创建一个极简对象(完全数据字典对象)没有原型(原型为 null)
let obj = {name:'kevin'};
console.log(obj.hasOwnProperty('name'));
let user = Object.create(null, {
name: {value: 'xj'}
});
// console.log(user.hasOwnProperty('name'));//Error
//Object.keys是静态方法,不是原型方法所以是可以使用的
console.log(Object.keys(user));
函数对象拥有多个原型
函数拥有两个原型,prototype
用于实例对象使用(用他这个构造函数new出来的对象),__proto__
用于函数对象使用(他自己)
系统构造函数的原型体现
声明的变量的类型,他的原型__proto__
指向创建其类型的构造函数(也可以叫类)的prototype
属性
prototype
属性的作用是为由这个构造函数创建的对象提供统一的方法
let arr = [];
console.log(arr.__proto__ === Array.prototype);//true
let obj = {};
console.log(obj.__proto__ === Object.prototype);//true
let str = '';
console.log(str.__proto__ === String.prototype);//true
原型链
调用对象上没有的方法,会在他的原型对象上找,找不到就在往上,以此类推。如果在Object构造函数的prototype中没找到,就返回null
构造函数的继承
new User()是对User构造函数的继承,那么我们只需要让Admin.prototype属性等于new User()就能让Admin继承User的方法和属性
function User(name, age){
this.name = name;
this.age = age;
this.show =()=>{
console.log(this.name,this.age);
}
}
function Admin(){}
Admin.prototype = new User('kevin',12);
let admin = new Admin();
admin.show();
new命令的原理
使用new
命令时,它后面的函数调用就不是正常的调用,而是依次执行下面的步骤。
- 创建一个空对象,作为将要返回的对象实例
- 将这个空对象的原型,指向构造函数的
prototype
属性 - 将这个空对象赋值给函数内部的
this
关键字 - 开始执行构造函数内部的代码
也就是说,构造函数内部,this
指的是一个新生成的空对象,所有针对this
的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this
对象),将其“构造”为需要的样子。
如果构造函数内部有return
语句,而且return
后面跟着一个对象,new
命令会返回return
语句指定的对象;否则,就会不管return
语句,返回this
对象。
构造函数
原型中的constructor
function User(){};
console.log(User.prototype.constructor === User);//true
如果更改User.prototype的指向,记得在对象中加上构造函数的属性,即constructor: User,
不然无法使用实例对象的constructor属性来构造新的实例对象。
我们也可以通过构造的实例对象找到构造函数,并且再次构造实例对象
function User(name,age){
this.name = name;
this.age = age;
this.show = ()=>{
return `${name}-${age}`;
}
};
let user = new User('kevin', 20);
function creatByObj(obj, ...args){
//声明一个常量等于该对象的原型的constructor(构造函数)
const constructor = Object.getPrototypeOf(obj).constructor;
return new constructor(...args);//返回一个通过constructor构造的实例对象
}
let admin = creatByObj(user, 'tom', 21);
console.log(admin.show());//tom-21
原型检测
instanceof 检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上
function A() {}
function B() {}
function C() {}
const c = new C();
B.prototype = c;
const b = new B();
A.prototype = b;
const a = new A();
console.dir(a instanceof A); //true
console.dir(a instanceof B); //true
console.dir(a instanceof C); //true
console.dir(b instanceof C); //true
console.dir(c instanceof B); //false
使用isPrototypeOf
检测一个对象是否是另一个对象的原型链中
function A() {}
function B() {}
function C() {}
const c = new C();
B.prototype = c;
const b = new B();
A.prototype = b;
const a = new A();
console.log(C.prototype.isPrototypeOf(b));//true
属性遍历
使用in
检测原型链上是否存在属性,使用 hasOwnProperty
只检测当前对象
let a = {url: 'dd'};
let b = {name: 'kevin'};
Object.setPrototypeOf(a,b);
console.log('name' in a);//true
console.log(a.hasOwnProperty('name'));//false
我们在使用for/in的时候,经常会看到这样的语句
let a = {url: 'dd'};
let b = {name: 'kevin'};
Object.setPrototypeOf(a,b);
for (const key in a) {
if (Object.hasOwnProperty.call(a, key)) {
const element = a[key];
console.log(element);//只输出dd (a.url)
}
}
如果没有该if语句,则会遍历a的原型链上的属性
let a = {url: 'dd'};
let b = {name: 'kevin'};
let c = {age: 22}
Object.setPrototypeOf(b,c)
Object.setPrototypeOf(a,b);
for (const key in a) {
console.log(a[key]);//换行输出dd(a.url) kevin(b.name) 22(c.age)
}
借用原型
使用 call
或 apply
可以借用其他原型方法完成功能。
下面的lesson没有max方法,但是可以通过arr.max.apply(或者call)来更改this的指向,使this指向lesson
let arr = {data: [1,2,3,4,5,6,7]};
Object.setPrototypeOf(arr,{
max(){
return this.data.sort((a, b) => b - a)[0];
}
});
console.log(arr.max());//7
let lesson = {
thsLesson: {node: 77, ts: 66, js: 88},
get data(){
return Object.values(this.thsLesson);
}
};
console.log(arr.max.apply(lesson));//88
如果max方法需要传参,则可以这样写
let arr = {data: [1,2,3,4,5,6,7]};
Object.setPrototypeOf(arr,{
max(data){
return data.sort((a, b) => b - a)[0];
}
});
console.log(arr.max(arr.data));//7
let lesson = {
thsLesson: {node: 77, ts: 66, js: 88},
};
//因为只用传一个参数,所以用call。如果使用apply就变成了传递多个参数,因为Object.values返回值是个数组。在这里我们只需要传一个参数(并且是一个数组)
console.log(arr.max.call(null, Object.values(lesson.thsLesson)));//88
当然,我们也可以使用Math.max()
方法
let arr = {data: [1,2,3,4,5,6,7]};
console.log(Math.max(...arr.data));//7
let lesson = {
thsLesson: {node: 77, ts: 66, js: 88},
};
console.log(Math.max(...Object.values(lesson.thsLesson)));//88
下面是获取设置了 class
属性的按钮,但 DOM 节点不能直接使用数组的filter
等方法,但借用数组的原型方法就可以操作了。
<body>
<button>1</button>
<button class="dd">2</button>
</body>
<script>
let btns = document.querySelectorAll('button');
console.log('filter' in btns);//btns原型链上没有filter方法
console.log('filter' in []);//数组的原型链上有filter方法
//所以我们可以通过call或者apply来借用方法
btns = [].filter.call(btns, item => item.hasAttribute('class'));
console.log(btns);//[button.dd]
</script>
使用优化
使用构造函数会产生函数复制造成内存占用,我们可以将方法写入构造函数的prototype属性中。
function User(name){
this.name = name;
}
User.prototype = {
//记得加上constructor属性,不然不能通过实例对象的constructor属性构建新的实例对象
constructor: User,
show(){
console.log(this.name);
},
get(){
console.log('get...');
}
};
let zs = new User('张三');
zs.show();
let ls = new User('李四');
ls.show();
原型总结
this与原型没啥关系
this与原型没啥关系,因为this指向的是调用该属性的对象,或者是call/apply/bind新的指向
不要滥用原型
如果引用了第三方库,可能属性会同名,后加载的会覆盖之前的。所以要慎用原型。
Object.create和\_proto_
使用Object.create
创建一个新对象时使用现有对象做为新对象的原型对象
第二个参数为设置新对象的属性(一个对象)
__proto__
属性只有浏览器必须部署,在非浏览器的环境不一定部署。
let user = {
show(){
return this.name;
}
};
let admin = Object.create(user, {
name: {
value: 'adminadmin'
}
});
// let admin = {name: 'adminadmin'};
// admin.__proto__ = user;
console.log(admin.show());//adminadmin
console.log(admin);//{name: 'adminadmin'}
console.log(admin.__proto__);//{show: ƒ}
Object.setPrototypeOf
关于原型的设置,也可以使用Object.setPrototypeOf
方法
let user = {
show(){
return this.name;
}
};
Object.setPrototypeOf(admin, user);
console.log(admin.show());//adminadmin
console.log(admin);//{name: 'adminadmin'}
console.log(admin.__proto__);//{show: ƒ}
\__proto__是一个属性访问器
__proto__
实际上是一个属性访问器,只允许对象或 null设置\__proto__属性。
如果想要让obj的\__proto__属性等于一个字符串或者数值,可以改变obj的原型。
let obj = {};
obj.__proto__ = 'kevin';
console.log(obj.__proto__ === 'kevin');//false
Object.setPrototypeOf(obj, null);
obj.__proto__ = 'kevin';
console.log(obj.__proto__ === 'kevin');//true
继承与多态
当对象中没使用的属性时,JS 会从原型上获取这就是继承在 JavaScript 中的实现
体验继承
function A(){
this.name = 'a';
}
function B(){
this.age = 22
}
B.prototype = new A();
let b = new B();
console.log(b.name);// a
继承是原型的继承,而不是改变构造函数的原型
下面是把某构造函数的prototype属性添加到原型链上
function User(){
this.name = 'kevin';
}
function Admin(){}
//在原型上增加方法,通过原型的继承(让构造函数的prototype属性的原型为另一个构造函数的prototype属性)
//在原型链上增加原型后,不会使role方法不能被调用
Admin.prototype.role = function(){
console.log('a role');
}
Admin.prototype.__proto__ = User.prototype;
let admin = new Admin();
admin.role();// a role
也可以使用Object.crate在原型链上添加构造函数的prototype。
但是需要添加构造函数的属性,因为crate是创建了一个新的对象。
在添加完构造函数属性后,还需要设置该属性不能被遍历。
方法重写
下而展示的是子类需要重写父类方法的技巧。
function Person() {}
Person.prototype.getName = function() {
console.log("parent method");
};
function User(name) {}
User.prototype = Object.create(Person.prototype);
User.prototype.constructor = User;
User.prototype.getName = function() {
//调用父级同名方法
Person.prototype.getName.call(this);
console.log("child method");
};
let hd = new User();
hd.getName();
多态
根据多种不同的形态产生不同的结果,下而会根据不同形态的对象得到了不同的结果。
function User() {}
User.prototype.show = function() {
console.log(this.description());
};
function Admin() {}
Admin.prototype = Object.create(User.prototype);
Admin.prototype.description = function() {
return "管理员在此";
};
function Member() {}
Member.prototype = Object.create(User.prototype);
Member.prototype.description = function() {
return "我是会员";
};
function Enterprise() {}
Enterprise.prototype = Object.create(User.prototype);
Enterprise.prototype.description = function() {
return "企业帐户";
};
for (const obj of [new Admin(), new Member(), new Enterprise()]) {
obj.show();
}
深挖继承
继承是为了复用代码,继承的本质是将原型指向到另一个对象。
调用父类构造函数完成对象的属性初始化
我们希望调用父类构造函数完成对象的属性初始化。
下面是使用示例,我们可以使用call或者apply来使User的this指向Admin
function User(name,age){
this.name = name;
this.age = age;
}
User.prototype.show = function(){
console.log(this.name,this.age);
};
function Admin(...args){
User.apply(this,args);
}
Admin.prototype.__proto__ = User.prototype;
let admin = new Admin('kevin', 20);
admin.show();//kevin 20
原型工厂
原型工厂是将继承的过程封装,使用继承业务简单化。
function extend(sub, sup){
sub.prototype = Object.create(sup.prototype);
sub.prototype.constructor = sub;
}
function Access(){};
function User(){};
function Admin(){};
function Member(){};
extend(User, Access);//User继承Access
extend(Admin, User);//Admin继承User
extend(Member, Access)//Member继承Access
Access.prototype.role = function(){};
User.prototype.getItem = function(){};
console.log(new Admin());// 继承关系: Admin>User>Access>Object
console.log(new Member());//继承关系:Member>Access>Object
对象工厂
在原型继承基础上,将对象的生成使用函数完成,并在函数内部为对象添加属性或方法。
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.show = function() {
console.log(this.name, this.age);
};
function Admin(name, age) {
let instance = Object.create(User.prototype);
User.call(instance, name, age);
instance.role=function(){
console.log('admin.role');
}
return instance;
}
let hd = Admin("后盾人", 19);
hd.show();
function member(name, age) {
let instance = Object.create(User.prototype);
User.call(instance, name, age);
return instance;
}
let lisi = member("李四", 28);
lisi.show();
Mixin 模式
JS
不能实现多继承,如果要使用多个类的方法时可以使用mixin
混合模式来完成。
mixin
类是一个包含许多供其它类使用的方法的类mixin
类不用来继承做为其它类的父类
下面是示例中 Admin
需要使用 Request.prototype
与 Credit
的功能,因为JS
是单继承,我们不得不将无关的类连接在一下,显然下面的代码实现并不佳
function extend(sub, sup) {
sub.prototype = Object.create(sup.prototype);
sub.prototype.constructor = sub;
}
function Credit() {}
function Request() {}
function User(name, age) {
this.name = name;
this.age = age;
}
extend(Request, Credit);
extend(User, Request);
Credit.prototype.total = function() {
console.log("统计积分");
};
Request.prototype.ajax = function() {
console.log("请求后台");
};
User.prototype.show = function() {
console.log(this.name, this.age);
};
function Admin(...args) {
User.apply(this, args);
}
extend(Admin, User);
let hd = new Admin("向军", 19);
hd.show();
hd.total(); //统计积分
hd.ajax(); //请求后台
下面分拆功能使用 Mixin 实现多继承,使用代码结构更清晰。只让 Admin
继承 User
原型
function extend(sub, sup) {
sub.prototype = Object.create(sup.prototype);
sub.prototype.constructor = sub;
}
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.show = function() {
console.log(this.name, this.age);
};
const Credit = {
total() {
console.log("统计积分");
}
};
const Request = {
ajax() {
console.log("请求后台");
}
};
function Admin(...args) {
User.apply(this, args);
}
extend(Admin, User);
Object.assign(Admin.prototype, Request, Credit);
let hd = new Admin("向军", 19);
hd.show();
hd.total(); //统计积分
hd.ajax(); //请求后台
mixin
类也可以继承其他类,比如下面的 Create
类获取积分要请求后台,就需要继承 Request
来完成。
super
是在mixin
类的原型中查找,而不是在User
原型中
function extend(sub, sup) {
sub.prototype = Object.create(sup.prototype);
sub.prototype.constructor = sub;
}
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.show = function() {
console.log(this.name, this.age);
};
const Request = {
ajax() {
return "请求后台";
}
};
const Credit = {
__proto__: Request,
total() {
console.log(super.ajax() + ",统计积分");
}
};
function Admin(...args) {
User.apply(this, args);
}
extend(Admin, User);
Object.assign(Admin.prototype, Request, Credit);
let hd = new Admin("向军", 19);
hd.show();
hd.total(); //统计积分
hd.ajax(); //请求后台
mixin模式的核心就是将一些功能剥离出来,然后再压入构造函数的prototype属性这个对象中