理解原型

在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中,我们可以通过原型来模拟类的声明,但是从上面学习的原型来看,还有有两个地方需要改进:

  1. 没有构造函数,同时也不能给构造函数传递参数。
  2. 所有属性共享,对于引用类型的属性,比如数组,是不能共享的,应该是每个实例各自持有一份独立存在于内存的属性。

解决方法并不复杂,请看实现:

 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.
  1. 我们定义的函数就可以作为构造函数,同时可以支持传递参数。
  2. 类的属性直接添加到当前对象上,不放在共享的原型对象中,只有方法才放到原型对象中。

稳妥对象(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

好吧,这种方式不但没有解决属性共享的问题,每次创建一个子类都需要重新创建对应的方法,这个也不是好方法,我们接着往下看。

组合继承的小问题

组合继承已经解决了继承的主要问题,但是还是存在两个小问题,而接下来我们将使用寄生+组合的方式来解决这两个小问题。

  1. 失去了默认创建的原型的constructor属性,需要手动加上;
  2. 原型使用父类的实例,导致原型中包含并不需要的属性及值(原型中只需要共享方法,但是比如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核心知识点】的更多相关文章

  1. python3.4学习笔记(十六) windows下面安装easy_install和pip教程

    python3.4学习笔记(十六) windows下面安装easy_install和pip教程 easy_install和pip都是用来下载安装Python一个公共资源库PyPI的相关资源包的 首先安 ...

  2. JVM学习笔记-第六章-类文件结构

    JVM学习笔记-第六章-类文件结构 6.3 Class类文件的结构 本章中,笔者只是通俗地将任意一个有效的类或接口锁应当满足的格式称为"Class文件格式",实际上它完全不需要以磁 ...

  3. (C/C++学习笔记) 十六. 预处理

    十六. 预处理 ● 关键字typeof 作用: 为一个已有的数据类型起一个或多个别名(alias), 从而增加了代码的可读性. typedef known_type_name new_type_nam ...

  4. python cookbook第三版学习笔记十六:抽象基类

    假设一个工程中有多个类,每个类都通过__init__来初始化参数.但是可能有很多高度重复且样式相同的__init__.为了减少代码.我们可以将初始化数据结构的步骤归纳到一个单独的__init__函数中 ...

  5. JavaScript权威设计--CSS(简要学习笔记十六)

    1.Document的一些特殊属性 document.lastModified document.URL document.title document.referrer document.domai ...

  6. Python学习笔记(六)——类和对象

    1.self的用法 全面理解self 2. 继承 子类继承父类,自动拥有父类的全部方法 >>> class Animal: def run(self): print('Animal ...

  7. python 学习笔记十六 django深入学习一 路由系统,模板,admin,数据库操作

    django 请求流程图 django 路由系统 在django中我们可以通过定义urls,让不同的url路由到不同的处理函数 from . import views urlpatterns = [ ...

  8. yii2源码学习笔记(十六)

    Module类的最后代码 /** * Registers sub-modules in the current module. * 注册子模块到当前模块 * Each sub-module shoul ...

  9. Swift学习笔记十六:协议

    Protocol(协议)用于统一方法和属性的名称,而不实现不论什么功能. 协议可以被类.枚举.结构体实现.满足协议要求的类,枚举,结构体被称为协议的遵循者. 遵循者须要提供协议指定的成员,如属性,方法 ...

  10. python cookbook第三版学习笔记十二:类和对象(三)创建新的类或实例属性

    先介绍几个类中的应用__getattr__,__setattr__,__get__,__set__,__getattribute__,. __getattr__:当在类中找不到attribute的时候 ...

随机推荐

  1. urllib2特点--超时设置

    # -*- coding: cp936 -*- #python 27 #xiaodeng #urllib2特点--超时设置 import urllib2 def urlopen(): url='htt ...

  2. C++ UTF8和UTF16互转代码

    简介 1.这段代码只考虑在小端序情况下的转换(一般的机器都是的). 2.这段代码需要C++11的支持(只是用到了u16string),如果不支持,可以添加下面代码 typedef uint16_t c ...

  3. adb forward交互流程

    命令:adb forward tcp:6100 tcp:7100 // PC上所有6100端口通信数据将被重定向到手机端7100端口server上 或者adb forward tcp:6100 loc ...

  4. loadrunner 脚本开发-参数化之将内容保存为参数、参数数组及参数值获取

    转自:http://blog.sina.com.cn/s/blog_13cc013b50102v49c.html(查看原文) 在VuGen中默认使用{}的字符串称为参数 注意:参数必须在双引号中才能用 ...

  5. navigationItem.rightBarButtonItem 设置背景图片,颜色更改解决的方法

    self.navigationItem.rightBarButtonItem=[[UIBarButtonItem alloc] initWithImage:[UIImage imageNamed:@& ...

  6. Java成神之路[转]

    阿里大牛珍藏架构资料,点击链接免费获取 针对本文,博主最近在写<成神之路系列文章> ,分章分节介绍所有知识点.欢迎关注. 主要版本 更新时间 备注 v1.0 2015-08-01 首次发布 ...

  7. Spring4+Hibernate4事务小记

    学习Spring+Hibernate,非常强大的框架,为了追新,就直接从最高版本开始学习了,这要冒很大的风险,因为网上可查到的资料大多是针对旧版本的,比如Spring3,Hibernate3. 根据我 ...

  8. inotify-tools命令使用讲解

    inotify-tools 是为linux下inotify文件监控工具提供的一套c的开发接口库函数,同时还提供了一系列的命令行工具,这些工具可以用来监控文件系统的事件. inotify-tools是用 ...

  9. POJ 2976 Dropping tests (0/1分数规划)

    Dropping tests Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 4654   Accepted: 1587 De ...

  10. ubuntu(14.04) 网路管理

    网络五元素: MAC地址 IP地址 网络掩码 网关 DNS:将ip地址转换成域名 ping ifconfig route /etc/resolv.conf netstat ip nmap cat /e ...