理解原型

在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. matplotlib01

    matplotlib是基于numpy的一套Python工具包.这个包提供了丰富的数据绘图工具,可实现数据分析的可视化. 所以在安装matplotlib时,需要先安装numpy包.

  2. Nginx 日志改成 JSON 格式

    Nginx 日志默认为普通文本的格式,例如,下面是 Nginx 的一行访问日志: 10.88.122.105 - - [02/Dec/2017:09:15:04 +0800] "GET /j ...

  3. Mac系统清理、占用空间大、空间不够、查看系统文件大小分布

    背景: 最近老提示空间不够,很尴尬,一直弹系统提示 如图,256的空间,就剩下几个G了,其中最大头的系统占用:160G,占比60%多 正常情况下:我们可以点击管理,进入到系统磁盘优化界面: 这种适用于 ...

  4. Android开发环境——Eclipse ADT相关内容汇总

     Android开发环境将分为SDK相关内容.Eclipse ADT相关内容.模拟器AVD相关内容.调试器DDMS相关内容.日志LogCat相关内容.连接驱动ADB相关内容.内存泄露检测工具MAT相关 ...

  5. DPDK的安装与绑定网卡(转)

    from:http://www.cnblogs.com/mylinuxer/p/4274178.html DPDK的安装与绑定网卡 DPDK的安装有两种方法: 第一种是使用dpdk/tools/set ...

  6. 自动化部署必备技能—定制化RPM包

    回顾下安装软件的三种方式: 1.编译安装软件,优点是可以定制化安装目录.按需开启功能等,缺点是需要查找并实验出适合的编译参数,诸如MySQL之类的软件编译耗时过长. 2.yum安装软件,优点是全自动化 ...

  7. linux下磁盘相关工具(待整理)

    一.概述: fsck tune2fs mke2fs badblocks mkfs* fdisk mount umount mknod e2label blkid hdparm mkswap swapo ...

  8. OpenCV 学习笔记03 凸包convexHull、道格拉斯-普克算法Douglas-Peucker algorithm、approxPloyDP 函数

    凸形状内部的任意两点的连线都应该在形状里面. 1 道格拉斯-普克算法 Douglas-Peucker algorithm 这个算法在其他文章中讲述的非常详细,此处就详细撰述. 下图是引用维基百科的.ε ...

  9. 【转】Markdown 的一些问题

    Markdown 的一些问题 把我之前的博文基本上转换成了 markdown 格式.我发现 markdown 虽然在编辑器里看起来比 HTML 清晰一些,但也有一些不足. 这些 markup 语言的格 ...

  10. Android KLog源代码分析

    Android KLog源代码分析 Android KLog源代码分析 代码结构 详细分析 BaseLog FileLog JsonLog XmlLog 核心文件KLogjava分析 遇到的问题 一直 ...