写在之前
距离上次整理日记整整一个周了,本来想再上周把这部分的内容完成,兑现之前吹过的牛皮。迟迟没有写一是因为上周项目比较忙,另一个是这一部分真的不好写,我写着写着感觉这一块的内容都不确定了(自己的基础真的有点差),去验证自己的想法的时候花了不少时间。这次的内容以《Javascript高级程序设计》第六章为主,先说原型与原型链,再说常用的继承的方式以及优缺点,最后结合实例看原型与继承在网页编程中的实际应用。
原型基础
原型对象
Javascript是面向对象的语言,在JS的世界里面万物皆对象。每个对象都有一个原型 prototype
对象,通过函数创建的对象也将拥有这个原型对象。原型是一个指向对象的指针。
- 可以将原型理解为对象的父亲,对象从原型对象继承属性。
- 原型就是对象,除了是某个对象的父母之外没什么特别之处。
- Object是所有对象的爸爸(爷爷,祖宗),所以对象都可以使用
toString/toValues/isPrototypeOf
等方法(子代可以使用父辈的资产)。 - 使用原型可以解决,通过构造函数创造对象的时复制多个函数造成的内存占用问题。
- 原型包含
constructor
属性,指向构造函数 - 对象包含
__proto__
指向他的原型对象
下例就是使用数组原型对象的concat
方法完成的连接操作。
let arr = ["a"];
console.log(arr.concat("b"));
如果 console.dir(arr)
我们就会发现他的结构信息
可以看到__proto__
指向原型对象,原型对象包含 constructor
它指向构造函数。上面的内容可以 arr.__proto__ 指向 Array.prototype 即 arr.__proto__ === Array.protptype
和 Array.prototype.constructor 指向 Array()
默认情况下创建的对象都是有原型的。obj的原型都为元对象Object,可以用 Object.getPrototypeOf(obj)
检验,它会返回 true。
我们也可以创建一个极简对象(纯数据字典对象),没有原型(原型为null)
let obj = Object.create(null,{
name:{
value:'zhangsan'
}
})
console.log(obj.hasOwnProperty('name')); //Error
prototype
用于实例对象使用,__prototype__
用于函数对象使用。JS万物皆对象,构造函数User 本身就是一个对象,通过构造函数User new出来的对象就是构造函数User的实例对象。
function User() {}
User.__proto__.view = function() {
console.log("User function view method");
};
User.view(); //User function view method
User.prototype.show = function() {
console.log("zhansan");
};
let user = new User();
user.show(); // zhangsan
console.log(User.prototype === user.__proto__); //true
下面是原型关系分析图
下面是使用构造函数床架对象的原型体现
- 构造函数拥有原型
-
创建对象时构造函数把原型对象赋予实例对象
function User() {}
let user = new User();
console.log(user.__proto__ === User.prototype); // true
下面使用数组会产生多级继承
let arr = [];
console.log(arr.__proto__ === Array.prototype); // true
console.log(Array.prototype.__proto__ === Object.prototype); //true
可以使用
setPrototypeOf
与 getPrototypeOf
获取与设置原型
let child = {};
let parent = {name:'parent'}
Object.setPrototype(child,parent);
console.log(Object.getPrototypeOf(child); //指向parent
下面使用自动构造函数创建的对象的原型体现可以参考图一。
function User(){}
let user = new User();
上面说到构造函数存在于原型对象中,是指向构造函数的引用
function User() {
this.show = function() {
return "show method";
};
}
const obj = new User(); //true
console.log(obj instanceof User); //true
console.log(obj.__proto__.constructor === User); //true
console.log(User.prototype.constructor === User); //true
const obj2 = new obj.constructor();
console.dir(obj2.show()); //show method
既然 constructor 是指向构造函数的引用,那我们就可以使用 constructor 创建对象
function User(name, age) {
this.name = name;
this.age = age;
}
function createByObject(obj, ...args) {
const constructor = Object.getPrototypeOf(obj).constructor;
return new constructor(...args);
}
let obj1 = new User("sinochem");
let obj2 = createByObject(obj1, "sinochemtech", 12);
console.log(xj);
原型链
通过引用类型的原型,继承另一个引用类型的属性和方法,这个也是实现继承的步骤
使用Object.setPrototypeOf可是设置对象的原型,下面的示例中继承关系为 obj > child > parent。
Object.getPrototype 用于获取一个对象的原型。
let obj = {
name: "zhangsan"
};
let child = {
ccc: "child-child"
};
let parent = {
ppp: "parent-parent"
};
//让obj继承hd,即设置obj的原型为hd
Object.setPrototypeOf(obj, child);
Object.setPrototypeOf(child, parent);
console.log(obj.ccc); // child-child
console.log(Object.getPrototypeOf(child) == parent); //true
原型检测
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
检测一个对象是否在另一个对象的原型链中
const a = {};
const b = {};
const c = {};
Object.setPrototypeOf(a, b);
Object.setPrototypeOf(b, c);
console.log(b.isPrototypeOf(a)); //true
console.log(c.isPrototypeOf(a)); //true
console.log(c.isPrototypeOf(b)); //true
属性遍历
使用in
检测原型链上是否存在属性,使用 hasOwnProperty
只检测当前对象
let a = { url: "baidu.com" };
let b = { name: "百度" };
Object.setPrototypeOf(a, b);
console.log("name" in a);
console.log(a.hasOwnProperty("name")); //true
console.log(a.hasOwnProperty("url")); //true
使用 for/in
遍历时同时会遍历原型上的属性如下例:
let parent = { name: "zhangsan" };
let child = Object.create(parent, {
url: {
value: "www.baidu.com",
enumerable: true
}
});
for (const key in child) {
console.log(key);
}
hasOwnProperty
方法判断对象是否存在属性,而不会查找原型。所以如果只想遍历对象属性使用以下代码:
let parent = { name: "后盾人" };
let child = Object.create(parent, {
url: {
value: "baidu.com",
enumerable: true
}
});
for (const key in child) {
if (child.hasOwnProperty(key)) {
console.log(key);
}
}
借用原型
使用 call
或 apply
可以借用其他原型方法完成功能。
下面的bar对象不能使用max
方法,但可以借用 foo 对象的原型方法
let foo = {
data: [1, 2, 3, 4, 5]
};
Object.setPrototypeOf(foo, {
max: function(data) {
return data.sort((a, b) => b - a)[0];
}
});
console.log(hd.max(hd.data));
let bar = {
lessons: { js: 100, php: 78, node: 78, linux: 125 }
};
console.log(foo.__proto__.max.call(bar, Object.values(bar.lessons)));
因为 Math.max
就是获取最大值的方法,所以代码可以再次简化
let foo = {
data: [1, 2, 3, 4, 5]
};
console.log(Math.max.apply(null, Object.values(foo.data)));
let bar = {
lessons: { js: 100, php: 78, node: 78, linux: 125 }
};
console.log(Math.max.apply(bar, Object.values(bar.lessons)));
下面是获取设置了 class
属性的按钮,但DOM节点不能直接使用数组的filter
等方法,但借用数组的原型方法就可以操作了。
原型总结
函数也是对象,但是比较特殊,有多个原型(__proto__
与prototype
)两个一定要分清楚。
通常说为构造函数设置原型指的是设置 prototype
当使用构造函数创建对象时把这个原型赋给这个对象。
function User(name) {
this.name = name;
}
User.prototype = {
show() {
return this.name;
}
};
let user = new User("zhangsan");
console.log(user.show());
函数默认prototype
指包含一个属性 constructor
的对象,constructor
指向当前构造函数
function User(name) {
this.name = name;
}
let user = new User("向军");
console.log(uer);
console.log(User.prototype.constructor == User); //true
console.log(user.__proto__ == User.prototype); //true
let lisi = new user.constructor("李四");
console.log(lisi.__proto__ == user.__proto__); //true
原型中保存引用类型会造成对象共享属性,所以一般只会在原型中定义方法。
function User() {}
User.prototype = {
lessons: ["JS", "VUE"]
};
const lisi = new User();
const wangwu = new User();
lisi.lessons.push("CSS");
console.log(lisi.lessons); //["JS", "VUE", "CSS"]
console.log(wangwu.lessons); //["JS", "VUE", "CSS"]
为Object原型对象添加方法,将影响所有函数
了解了原型后可以为系统对象添加方法,比如为字符串添加了一截断函数。
String.prototype.truncate = function (len = 5) {
return this.length <= len ? this : this.substr(0, len) + '...';
}
console.log('1234567890'.truncate(3)); //123...
使用 Object.create
创建一个新对象时使用现有对象做为新对象的原型对象。它第一个参数必须是一个对象或者null(没有原型的对象)。
使用Object.create
设置对象原型
let user = {
show() {
return this.name;
}
};
let obj = Object.create(user);
obj.name = "zhangsan";
console.log(obj.show());
在设置时使用第二个参数设置新对象的属性
let user = {
show() {
return this.name;
}
};
let obj = Object.create(user, {
name: {
value: "后盾人"
}
});
console.log(obj);
在实例化对象上存在__proto__
记录了原型,所以可以通过对象访问到原型的属性或方法。
-
__proto__
不是对象属性,理解为prototype
的getter/setter
实现,他是一个非标准定义 -
__proto__
内部使用getter/setter
控制值,所以只允许对象或null - 建议使用
Object.setPrototypeOf
与Object.getProttoeypOf
替代__proto__
下面修改对象的 __proto__
是不会成功的,因为_proto__
内部使用getter/setter
控制值,所以只允许对象或null。
let obj = {};
obj.__proto__ = "123";
console.log(obj);
下面定义的__proto__
就会成功,因为这是一个极简对象,没有原型对象所以不会影响__proto__
赋值。
let obj = Object.create(null);
obj.__proto__ = "123";
console.log(obj); //{__proto__: "123"}
下面通过改变对象的 __proto__
原型对象来实现继承,继承可以实现多层,
let person = {
name: "zhangsan"
};
let bar = {
show() {
return this.name;
}
};
let foo = {
handle() {
return `用户: ${this.name}`;
}
};
bar.__proto__ = foo;
person.__proto__ = bar;
console.log(person.show());
console.log(person.handle());
console.log(person);
构造函数中的 __proto__
使用
function User(name, age) {
this.name = name;
this.age = age;
}
User.prototype.show = function () {
return `姓名:${this.name},年龄:${this.age}`;
};
let lisi = new User('李四', 12);
let wangwu = new User('王武', 16);
console.log(lisi.__proto__ == User.prototype); //true
可以使用 __proto__
或 Object.setPrototypeOf
设置对象的原型,使用Object.getProttoeypOf
获取对象原型。
function Person() {
this.getName = function() {
return this.name;
};
}
function User(name, age) {
this.name = name;
this.age = age;
}
let lisi = new User("李四", 12);
Object.setPrototypeOf(lisi, new Person());
console.log(lisi.getName()); //李四
对象设置属性,只是修改对象属性并不会修改原型属性,使用hasOwnProperty
判断对象本身是否含有属性并不会检测原型。
function User() {}
const lisi = new User();
const wangwu = new User();
lisi.name = "李四";
console.log(lisi.name); //李四
console.log(lisi.hasOwnProperty("name")); //true
//修改原型属性后
lisi.__proto__.name = "张三";
console.log(wangwu.name); //张三
//删除对象属性后
delete lisi.name;
console.log(lisi.hasOwnProperty("name")); //false
console.log(lisi.name); // 张三
使用 in
会检测原型与对象,而 hasOwnProperty
只检测对象,所以结合后可判断属性是否在原型中
使用建议:通过前介绍我们知道可以使用多种方式设置原型
-
prototype
构造函数的原型属性 -
Object.create
创建对象时指定原型 -
__proto__
声明自定义的非标准属性设置原型,解决之前通过Object.create
定义原型,而没提供获取方法 -
Object.setPrototypeOf
设置对象原型
这几种方式都可以管理原型,一般以我个人情况来讲使用 prototype
更改构造函数原型,使用 Object.setPrototypeOf
与 Object.getPrototypeOf
获取或设置原型。
构造函数
原型属性
构造函数在被new
时把构造函数的原型(prototype)赋值给新对象。如果对象中存在属性将使用对象属性,不再原型上查找方法。
构造函数只会产生一个原型对象
function foo() {
this.show = function() {
return "show in object";
};
}
foo.prototype.show = function() {
return "show in prototype";
};
const obj = new foo();
console.log(obj.show());
对象的原型引用构造函数的原型对象,是在创建对象时确定的,当构造函数原型对象改变时会影响后面的实例对象。
function foo() {}
foo.prototype.name = "foofoo";
const obj1 = new foo();
console.log(obj1.name); //foofoo
foo.prototype = {
name: "123"
};
const obj2 = new hd();
console.dir(obj2.name); //123
以下代码直接设置了构造函数的原型将造成 constructor
丢失
function User(name) {
this.name = name;
}
User.prototype = {
show: function() {}
};
let u1 = new User("u1u1");
let u2 = new u1.constructor("u2u2");
console.log(u2); //String {"u2u2"}
正确的做法是要保证原型中的 constructor
指向构造函数
function User(name) {
this.name = name;
}
User.prototype = {
constructor: User,
show: function() {}
};
let u1 = new User("u1u1");
let u2 = new hd.constructor("u2u2");
console.log(u2);
构造函数的优化使用
使用构造函数会产生函数复制造成内存占用,及函数不能共享的问题。
function User(name) {
this.name = name;
this.get = function() {
return this.name;
};
}
let lisi = new User("小明");
let wangwu = new User("王五");
console.log(lisi.get == wangwu.get); //false
将方法定义在原型上为对象共享,解决通过构造函数创建对象函数复制的内存占用问题
function User(name) {
this.name = name;
}
User.prototype.get = function() {
return "姓名:" + this.name;
};
let lisi = new User("小明");
let wangwu = new User("王五");
console.log(lisi.get == wangwu.get); //true
//通过修改原型方法会影响所有对象调用,因为方法是共用的
lisi.__proto__.get = function() {
return "姓名123:" + this.name;
};
console.log(lisi.get());
console.log(wangwu.get());
下面演示使用原型为多个实例共享属性
function User(name, age) {
this.name = name;
this.age = age;
this.show = () => {
return `你在${this.site}的姓名:${this.name},年龄:${this.age}`;
}
}
User.prototype.site = '中化';
let lisi = new User('李四', 12);
let xiaoming = new User('小明', 32);
console.log(lisi.show()); //你在中化的姓名:李四,年龄:12
console.log(xiaoming.show()); //你在中化的姓名:小明,年龄:32
使用Object.assign
一次设置原型方法来复用,后面会使用这个功能实现Mixin模式
function User(name, age) {
this.name = name;
this.age = age;
}
Object.assign(User.prototype, {
getName() {
return this.name;
},
getAge() {
return this.age;
}
});
let lisi = new User('李四', 12);
let xiaoming = new User('小明', 32);
console.log(lisi.getName()); //李四
console.log(lisi.__proto__)
通过上面这种方法设置可以避《JavaScript高级程序设计》提到了通过原型字面量批量设置原型带来的构造函数指针问题和如果在字面量设置原型之前在原型上设置方法的方法丢失问题(这是一个顺序问题,因为原型指向了新对象,原原型对象就不再生效了)。如果想了解那一部分的内容还是建议读一下高级编程这本书。
继承
体验继承
下面为 Stu
更改了原型为User
的实例对象,lisi
是通过构造函数Stu
创建的实例对象
-
lisi
在执行getName
方法时会从自身并向上查找原型,这就是原型链特性 - 当然如果把
getName
添加到对象上,就不继续追溯原型链了
function User() {}
User.prototype.getName = function() {
return this.name;
};
function Stu(name) {
this.name = name;
}
Stu.prototype = new User();
const lisi = new Stu("李四");
console.log(lisi.__proto__);
console.log(lisi.getName());
当对象中没使用的属性时,JS会从原型上获取这就是继承在JavaScript中的实现。
继承实现
下面使用Object.create
创建对象,做为Admin、Member
的原型对象来实现继承。
function User() {}
User.prototype.getUserName = function() {};
function Admin() {}
Admin.prototype = Object.create(User.prototype);
Admin.prototype.role = function() {};
function Member() {}
Member.prototype = Object.create(User.prototype);
Member.prototype.email = function() {};
console.log(new Admin());
console.log(new Member());
不能使用以下方式操作,因为这样会改变User的原型方法,这不是继承,这是改变原型
...
function User() {}
User.prototype.getUserName = function() {};
function Admin() {}
Admin.prototype = User.prototype;
Admin.prototype.role = function() {};
...
上一节提到有多种方式通过构造函数创建对象
function Admin() {}
console.log(Admin == Admin.prototype.constructor); //true
let obj1 = new Admin.prototype.constructor();
console.log(obj1);
let obj2 = new Admin();
console.log(obj2);
因为有时根据得到的对象获取构造函数,然后再创建新对象所以需要保证构造函数存在,但如果直接设置了 Admin.prototype
属性会造成constructor
丢失,所以需要再次设置constructor
值。
function User() {}
function Admin() {}
Admin.prototype = Object.create(User.prototype);
Admin.prototype.role = function() {};
let obj1 = new Admin();
console.log(obj1.constructor); //constructor丢失,返回User构造函数
Admin.prototype.constructor = Admin;
let obj2 = new Admin();
console.log(obj2.constructor); //正确返回Admin构造函数
//现在可以通过对象获取构造函数来创建新对象了
console.log(new obj2.constructor());
上面通过显示的方式指定了 constructor
导致成了 constructor
成为了可遍历的属性,我们可以使用Object.defineProperty
定义来禁止遍历constructor属性。
function User() {}
function Admin(name) {
this.name = name;
}
Admin.prototype = Object.create(User.prototype);
Object.defineProperty(Admin.prototype, "constructor", {
value: Admin,
enumerable: false //禁止遍历
});
let hd = new Admin("伟哥有话说");
for (const key in hd) {
console.log(key);
}
刚次提到这种继承方式就是《JavaScript高级程序编程》中提到寄生组合继承。该继承方式是开发人员认为最理想的继承方式。
function SuperType(name){
this.name = name;
this.colors = ['red', 'blue', 'green']
}
SuperType.prototype.sayName = function(){
alert(this.name)
};
function SubType(name,age){
SubType.call(this,name);
this.age = age;
}
// 继承方法
SubType.prototype = Object.create(SuperType.prototype);
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
}
let instance = new SubType('Nocholas',26)
instance.sayName();
instance.sayAge();
对于这种继承可以如下封装:
function inheritPrototype(subType, superType){
const prototype = Object.create(superType);
prototype.constructor = subType;
subType.prototype = prototype;
}
看过《JavaScript高级程序编程》的同学可能觉得这个上面这个封装及熟悉又陌生。这个就是你觉得的那个,变得就是Object.create
。其实这个就是书上的实现。冷不丁的是不是又搞清楚了一个原理。下面这个写法更简单。
function inheritPrototype(subType, superType){
subType.prototype = Object.create(superType);
subType.prototype.constructor = subType;
}
方法重写
下而展示的是子类需要重写父类方法的技巧。
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 instance = new User();
instance.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
制作选项卡
组合继承
用户管理
配置管理
用户管理
配置管理
写在最后
这一篇终于写完了。。感觉对JS这个语言重新熟悉了一遍。ES6逐渐普及,新式的语法让继承变得很简单,似乎这些东西不用我们再去考虑了。但是理解原理会对这门语言更加深刻。JavaScript相对其他编程语言来讲,灵活的难以想象,给我一个对象,就能还你一个世界。