HTML5学习笔记(十六):原型、类和继承【JS核心知识点】
理解原型
在JavaScript中,只要声明了一个函数,就会为该函数创建一个名为prototype的属性,该属性指向当前函数的原型对象。
而函数的原型对象有一个constructor属性,该属性指向刚声明的函数。
需要注意的是:只有通过声明创建的函数对象才会具有原型对象和prototype属性,其它通过new关键字创建的对象都不具有prototype属性。
var obj = new Object();
console.log(obj.prototype); // undefined
var str = new String("abc");
console.log(str.prototype); // undefined
var arr = new Array();
console.log(arr.prototype); // undefined function Person() {}
//var Person = function() {} // 也可以用这种写法,效果一致
console.log(Person.prototype); // Object {}
var p = new Person();
console.log(p.prototype); // undefined
实例访问原型对象
我们通过声明的函数创建得到的实例,需要获取原型对象该怎么办,非标准方式__proto__属性在Chrome、Firefox和Safari中可以获得:
function Person() {}
var p = new Person();
console.log(p.__proto__ === Person.prototype); // true
当然,ECMA也有提供标准的方法来获取实例的原型对象,Object.getPrototypeOf方法:
function Person() {}
var p = new Person();
console.log(Object.getPrototypeOf(p) === Person.prototype); // true
原型对象有什么用?
原型对象上的属性是所有实例共享的,即所有实例对象可以像调用自身的属性及方法一样调用定义在原型对象上的属性及方法。
示例如下:
function Person() {
Person.prototype.name = "Tom";
Person.prototype.sayName = function() {
console.log(this.name);
};
}
var p1 = new Person();
var p2 = new Person();
p1.sayName(); // Tom
p2.sayName(); // Tom
Person.prototype.name = "Jake";
p1.sayName(); // Jake
p2.sayName(); // Jake
Person.prototype.sayName = function() {console.log("hello, " + this.name + "!")};
p1.sayName(); // hello, Jake!
p2.sayName(); // hello, Jake!
我们可以发现原型对象的属性改变会影响到所有的实例。
实例不能覆盖原型的属性
当实例对象尝试修改原型的属性时,实际上是将属性添加到自己身上,我们可以看下面脚本:
function Person() {
Person.prototype.name = "Tom";
}
var p = new Person();
console.log(p.name); // Tom
p.name = "Jake";
console.log(p.name); // Jake
console.log(Person.prototype.name); // Tom
delete p.name;
console.log(p.name); // Tom
判断属性是否存在
hasOwnProperty
不考虑原型的属性,仅判断当前对象是否有指定的属性。
in
判断当前对象及原型对象上是否有指定的属性。
function Person() {
Person.prototype.name = "Tom";
}
var p = new Person();
console.log(p.hasOwnProperty("name")); // false
console.log("name" in p); // true
p.name = "Jake";
console.log(p.hasOwnProperty("name")); // true
console.log("name" in p); // true
delete p.name;
console.log(p.hasOwnProperty("name")); // false
console.log("name" in p); // true
实例获取属性流程
console.log(p.name);这句脚本,p对象会先查找自身是否有name属性,有则返回,没有再查找p对象的原型对象是否有name属性,有则返回,还没有就查找Object原型对象身上是否有name属性,有则返回,还是没有则返回undefined。
function Person() {}
var p = new Person();
console.log(p.name); // undefined
Object.prototype.name = "Tom";
console.log(p.name); // Tom
类
在JavaScript中,我们可以通过原型来模拟类的声明,但是从上面学习的原型来看,还有有两个地方需要改进:
- 没有构造函数,同时也不能给构造函数传递参数。
- 所有属性共享,对于引用类型的属性,比如数组,是不能共享的,应该是每个实例各自持有一份独立存在于内存的属性。
解决方法并不复杂,请看实现:
function Person(name, age) {
this.name = name;
this.age = age;
this.data = ["China", "Shanghai"]; Person.prototype.sayHello = function() {
console.log("Hello, I am " + this.name + ", I am " + this.age + " old, I am from " + this.data[0] + " " + this.data[1] + ".");
}
} var p1 = new Person("Li Lei", 28);
p1.data[1] = "Guangzhou";
p1.sayHello(); // Hello, I am Li Lei, I am 28 old, I am from China Guangzhou.
var p2 = new Person("Han Meimei", 27);
p2.data[1] = "Beijing";
p2.sayHello(); // Hello, I am Han Meimei, I am 27 old, I am from China Beijing.
- 我们定义的函数就可以作为构造函数,同时可以支持传递参数。
- 类的属性直接添加到当前对象上,不放在共享的原型对象中,只有方法才放到原型对象中。
稳妥对象(Durable Objects)
在某些情况下,我们不希望我们的数据在外部被改变,就可以用上稳妥对象:
function Person(name) {
var o = new Object(); o.sayName = function() {
console.log(name);
} return o;
} var p = new Person("Li Lei");
p.sayName(); // Li Lei
p.name = "Tom";
p.sayName(); // Li Lei
p.__proto__.name = "Tom";
p.sayName(); // Li Lei
该方法特别适合在某些需要提供更安全的环境下使用。
继承
在JavaScript中,可以通过原型链来实现类似面向对象语言的继承。
原型链
ECMA中提供了原型链,用来作为实现继承的基础,那么什么是原型链呢?之前我们说过,当我们获取一个对象的属性时,会判断该对象是否存在属性,如果不存在则查找该对象的原型对象是否存在属性。那么,当对象的原型指向另一个对象时,就会出现一种类似于链表一样的数据结构,成为原型链。
看例子:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
} function SubType() {
this.subproperty = false;
}
//将子类的原型设定为父类的实例, 在此之前不要给子类的原型添加任何的属性或方法, 否则会被覆盖
SubType.prototype = new SuperType();
//为新的原型对象添加方法
SubType.prototype.getSubValue = function() {
return this.subproperty;
} var instance = new SubType();
console.log(instance.getSuperValue()); // true
我们看下图示:
这里就可以看做一个存在3个元素的原型链,因为所有的函数原型对象默认都指向Object对象。
我们下面以调用toString方法为例来看看一个方法在原型链上是如何查找的:首先查找instance对象是否有toString属性,没有查找原型对象SuperType的实例,发现也没有时,继续查找SuperType实例的原型对象,也没有发现,在向上查找就到了SuperType的原型Object类型对象了,在这里找到了toString方法,即可调用到该方法。如果还没有找到则返回undefined。
判断是否是指定对象的实例
使用instanceof关键字即可:
console.log(instance instanceof SubType); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof Object); // true
原型链实现继承的问题
使用原型链实现继承时会遇到2个问题,我们下面来看看:
引用类型共享
由于实例变成了原型对象,所以实例对象中的所有引用对象都会被共享,如下:
function SuperType() {
this.property = ["apple", "Banana"];
} function SubType() {
}
SubType.prototype = new SuperType(); var instance1 = new SubType();
var instance2 = new SubType();
instance1.property.push("orange");
console.log(instance1.property); // ["apple", "Banana", "orange"]
console.log(instance2.property); // ["apple", "Banana", "orange"]
这显然不是我们想要的效果。
如何向父类构造函数传递参数
如果父类是可以传递参数的,那么这种写法无法从子类向父类传递参数。
为了解决上面的问题,在实际开发中出现了多种继承方式,我们下面一个一个来看。
组合继承
属性使用组合调用的方式(属性不能共享直接添加到实例上),方法使用外部定义到原型的方式(方法需要共享)。
function SuperType(name) {
this.name = name;
this.colors = ["red", "green"];
} SuperType.prototype.sayName = function() {
console.log(this.name);
} function SubType(name, age) {
//调用父类构造函数方法, 可以将实例上的属性直接设置到新对象上
SuperType.call(this, name);
//这里可以添加自身拥有的属性
this.age = age;
} //子类原型指向父类实例
SubType.prototype = new SuperType();
//默认的原型对象呗覆盖, 需要加上constructor属性
SubType.prototype.constructor = SubType;
//子类的方法
SubType.prototype.sayAge = function() {
console.log(this.age);
} var obj = new SubType("Li Lei", 28);
obj.colors.push("blue");
obj.sayName(); // Li Lei
obj.sayAge(); //
console.log(obj.colors); // ["red", "green", "blue"]
var obj2 = new SubType("Han Meimei", 27);
obj2.colors.unshift("blue");
obj2.sayName(); // Han Meimei
obj2.sayAge(); //
console.log(obj2.colors); // ["blue", "red", "green"]
该方法简单易懂,可以说是JavaScript中用得最多的继承实现方法。
寄生组合继承
该模式是JavaScript开发中目前为止认为最理想的继承方式。
我们下面慢慢来分析该模式。
JavaScript领域大师道格拉斯·克罗克福德提出了一种继承方法,并给出了一个方法:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
实际上该方法就是创建了一个原型为参数的新空对象出来。
而在ECMAScript5里,已经对该方法进行了封装,Object.create方法即为该方法的官方提供版本,create方法额外提供了第二个参数,用来覆盖原型对象上的同名属性。
原型式继承
即使用Object.create方法来实现的继承,该方法实现的继承存在引用对象共享的问题,如下:
var person = {
name: "Le Lei",
colors: ["red", "green"]
}; var p1 = Object.create(person);
p1.name = "Tom";
p1.colors.push("blue"); var p2 = Object.create(person);
p2.name = "Han Meimei";
p2.colors.unshift("black"); console.log(person.colors); // ["black", "red", "green", "blue"]
console.log(p1.colors); // ["black", "red", "green", "blue"]
console.log(p2.colors); // ["black", "red", "green", "blue"]
目前看来这不是一种靠谱的继承方式,我们接着看。
寄生继承
该方式基于上面的实现提供了一个方法用来加强子类,如下:
var person = {
name: "Le Lei",
colors: ["red", "green"]
}; function createSubPerson(o) {
var clone = Object.create(o);
clone.sayName = function() {
console.log(this.name);
};
return clone;
} var p = createSubPerson(person);
p.sayName(); // Le Lei
好吧,这种方式不但没有解决属性共享的问题,每次创建一个子类都需要重新创建对应的方法,这个也不是好方法,我们接着往下看。
组合继承的小问题
组合继承已经解决了继承的主要问题,但是还是存在两个小问题,而接下来我们将使用寄生+组合的方式来解决这两个小问题。
- 失去了默认创建的原型的constructor属性,需要手动加上;
- 原型使用父类的实例,导致原型中包含并不需要的属性及值(原型中只需要共享方法,但是比如name等属性也被包含到原型对象了);
实现寄生组合继承
使用该模式可以解决组合继承的问题,同时可以得到组合继承的所有优点。
/**
* 将指定函数设定为另一个函数的原型对象, 可以实现继承功能
* @param subType 用做子类的函数
* @param superType 用做父类的函数
*/
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
}
以上的方法定义了用来实现继承的函数,第一行代码创建了superType原型对象的副本,第二行代码设定constructor指向当前函数对象,第三行代码将创建的对象赋予subType的原型作为subType的原型对象。
/**
* 将指定函数设定为另一个函数的原型对象, 可以实现继承功能
* @param subType 用做子类的函数
* @param superType 用做父类的函数
*/
function inheritPrototype(subType, superType) {
var prototype = Object.create(superType.prototype);
prototype.constructor = subType;
subType.prototype = prototype;
} function SuperType(name) {
this.name = name;
this.colors = ["red", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
} function SubType(name, age) {
// 调用父类构造函数添加不共享的属性
SuperType.call(this, name);
this.age = age;
} // 设定 SubType 继承自 SuperType
inheritPrototype(SubType, SuperType); // 添加SubType的方法, 一定要放在设置继承关系之后
SubType.prototype.sayAge = function() {
console.log(this.age);
} var instance = new SubType("Nicholas", 29);
instance.sayName(); // Nicholas
我们来看一下图示:
HTML5学习笔记(十六):原型、类和继承【JS核心知识点】的更多相关文章
- python3.4学习笔记(十六) windows下面安装easy_install和pip教程
python3.4学习笔记(十六) windows下面安装easy_install和pip教程 easy_install和pip都是用来下载安装Python一个公共资源库PyPI的相关资源包的 首先安 ...
- JVM学习笔记-第六章-类文件结构
JVM学习笔记-第六章-类文件结构 6.3 Class类文件的结构 本章中,笔者只是通俗地将任意一个有效的类或接口锁应当满足的格式称为"Class文件格式",实际上它完全不需要以磁 ...
- (C/C++学习笔记) 十六. 预处理
十六. 预处理 ● 关键字typeof 作用: 为一个已有的数据类型起一个或多个别名(alias), 从而增加了代码的可读性. typedef known_type_name new_type_nam ...
- python cookbook第三版学习笔记十六:抽象基类
假设一个工程中有多个类,每个类都通过__init__来初始化参数.但是可能有很多高度重复且样式相同的__init__.为了减少代码.我们可以将初始化数据结构的步骤归纳到一个单独的__init__函数中 ...
- JavaScript权威设计--CSS(简要学习笔记十六)
1.Document的一些特殊属性 document.lastModified document.URL document.title document.referrer document.domai ...
- Python学习笔记(六)——类和对象
1.self的用法 全面理解self 2. 继承 子类继承父类,自动拥有父类的全部方法 >>> class Animal: def run(self): print('Animal ...
- python 学习笔记十六 django深入学习一 路由系统,模板,admin,数据库操作
django 请求流程图 django 路由系统 在django中我们可以通过定义urls,让不同的url路由到不同的处理函数 from . import views urlpatterns = [ ...
- yii2源码学习笔记(十六)
Module类的最后代码 /** * Registers sub-modules in the current module. * 注册子模块到当前模块 * Each sub-module shoul ...
- Swift学习笔记十六:协议
Protocol(协议)用于统一方法和属性的名称,而不实现不论什么功能. 协议可以被类.枚举.结构体实现.满足协议要求的类,枚举,结构体被称为协议的遵循者. 遵循者须要提供协议指定的成员,如属性,方法 ...
- python cookbook第三版学习笔记十二:类和对象(三)创建新的类或实例属性
先介绍几个类中的应用__getattr__,__setattr__,__get__,__set__,__getattribute__,. __getattr__:当在类中找不到attribute的时候 ...
随机推荐
- 转 解决在X64 RedHat5.1 下以编译方式安装PHP 出现的种种问题
不知道大家有没有遇到在 X64 RedHat5 或者 RedHat4 下.编译安装PHP环境的时候. 安装了libxml,zlib,jpeg,libpng,freetype,libart_lgpl, ...
- 利用XAMPP搭建PHP开发环境,解决443端口被占用
为了方便,作为学习使用的PHP环境,我们可以直接使用Apache+mysql+php集成开发环境.这样的集成软件有appserv和xampp,这里我们以xampp为例. 首先下载xampp软件,下载地 ...
- Swift3 - compare方法之ComparisonResult说明
Swift3在实现两个对象比较时,引入了compare方法,其中,方法返回值ComparisonResult解释如下: ComparisonResult是一个枚举类型,包含了以下3个成员: 其中: q ...
- leetcode770. Basic Calculator IV
此题真可谓是练习编程语言的绝好材料 ! import java.util.*; class Solution { class Item { Map<String, Integer> var ...
- PIL笔记
图片颜色的类型 1 (1-bit pixels, black and white, stored with one pixel per byte) L (8-bit pixels, black and ...
- VS2010 C++环境下DLL和LIB文件的生成与调试
利用VS2010工具,调试DLL文件的方法现总结如下: 在一个解决方案中生成两个工程,假设MYDLL和MYDLG两个工程,前者是DLL工程,后者DLG调用前边的DLL工程.设置如下: 目录如下:图,本 ...
- SQL Server 表分区之水平表分区
什么是表分区? 表分区分为水平表分区和垂直表分区,水平表分区就是将一个具有大量数据的表,进行拆分为具有相同表结构的若干个表:而垂直表分区就是把一个拥有多个字段的表,根据需要进行拆分列,然后根据某一个字 ...
- 解决Winform程序在不同分辨率系统下界面混乱
问题分析: 产生界面混乱的主要原因是,winform程序的坐标是基于点(Point)的,而Point又与DPI相关,具体就是 一英寸 =72Points 一英寸 = 96pixel ...
- 搭建自己的 github.io 博客
1.前言 github.io 是基于 Github 的 repo 管理,这意味着咱们对其是有绝对的控制,这个跟放在第三方的平台比,可控性要好太多. 使用 github pages 服务搭建博客的好处有 ...
- 基于Amoba实现mysql主从读写分离
一.Amoeba简介 Amoeba是一个以MySQL为底层数据存储,并对应用提供MySQL协议接口的proxy.它集中地响应应用的请求,依据用户事先设置的规则,将SQL请求发送到特 ...