深入理解JS继承和原型链的问题


Posted in Javascript onDecember 17, 2016

对于那些熟悉基于类的面向对象语言(Java 或者 C++)的开发者来说,JavaScript 的语法是比较怪异的,这是由于 JavaScript 是一门动态语言,而且它没有类的概念( ES6 新增了class 关键字,但只是语法糖,JavaScript 仍旧是基于原型)。

涉及到继承这一块,Javascript 只有一种结构,那就是:对象。在 javaScript 中,每个对象都有一个指向它的原型(prototype)对象的内部链接。这个原型对象又有自己的原型,直到某个对象的原型为null 为止(也就是不再有原型指向),组成这条链的最后一环。这种一级一级的链结构就称为原型链(prototype chain)。

虽然,原型继承经常被视作 JavaScript 的一个弱点,但事实上,原型继承模型比经典的继承模型更强大。举例来说,在原型继承模型的基础之上建立一个经典的继承模型是相当容易的。

一、基于原型链的继承

1.继承属性

JavaScript 对象是动态的属性“包”(指其自己的属性)。JavaScript 对象有一个指向一个原型对象的链。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依此层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。下面的代码将演示,当访问一个对象的属性时会发生的行为:

// 假定有一个对象 o, 其自身的属性(own properties)有 a 和 b: 
// {a: 1, b: 2} 
// o 的原型 o.[[Prototype]]有属性 b 和 c: 
// {b: 3, c: 4} 
// 最后, o.[[Prototype]].[[Prototype]] 是 null. 
// 这就是原型链的末尾,即 null, 
// 根据定义,null 没有[[Prototype]]. 
// 综上,整个原型链如下:  
// {a:1, b:2} ---> {b:3, c:4} ---> null 
 
console.log(o.a); // 1 
// a是o的自身属性吗?是的,该属性的值为1 
 
console.log(o.b); // 2 
// b是o的自身属性吗?是的,该属性的值为2 
// o.[[Prototype]]上还有一个'b'属性,但是它不会被访问到.这种情况称为"属性遮蔽 (property shadowing)". 
 
console.log(o.c); // 4 
// c是o的自身属性吗?不是,那看看o.[[Prototype]]上有没有. 
// c是o.[[Prototype]]的自身属性吗?是的,该属性的值为4 
 
console.log(o.d); // undefined 
// d是o的自身属性吗?不是,那看看o.[[Prototype]]上有没有. 
// d是o.[[Prototype]]的自身属性吗?不是,那看看o.[[Prototype]].[[Prototype]]上有没有. 
// o.[[Prototype]].[[Prototype]]为null,停止搜索, 
// 没有d属性,返回undefined

创建一个对象它自己的属性的方法就是设置这个对象的属性。唯一例外的获取和设置的行为规则就是当有一个 getter或者一个setter 被设置成继承的属性的时候。

2.继承方法

JavaScript 并没有其他基于类的语言所定义的“方法”。在 JavaScript 里,任何函数都可以添加到对象上作为对象的属性。函数的继承与其他的属性继承没有差别,包括上面的“属性遮蔽”(这种情况相当于其他语言的方法重写)。

当继承的函数被调用时,this 指向的是当前继承的对象,而不是继承的函数所在的原型对象。

var o = { 
 a: 2, 
 m: function(){ 
  return this.a + 1; 
 } 
}; 
 
console.log(o.m()); // 3 
// 当调用 o.m 时,'this'指向了o. 
 
var p = Object.create(o); 
// p是一个对象, p.[[Prototype]]是o. 
 
p.a = 12; // 创建 p 的自身属性a. 
console.log(p.m()); // 13 
// 调用 p.m 时, 'this'指向 p.  
// 又因为 p 继承 o 的 m 函数 
// 此时的'this.a' 即 p.a,即 p 的自身属性 'a'

二、使用不同的方法来创建对象和生成原型链

1.使用普通语法创建对象

var o = {a: 1}; 
 
// o这个对象继承了Object.prototype上面的所有属性 
// 所以可以这样使用 o.hasOwnProperty('a'). 
// hasOwnProperty 是Object.prototype的自身属性。 
// Object.prototype的原型为null。 
// 原型链如下: 
// o ---> Object.prototype ---> null 
 
var a = ["yo", "whadup", "?"]; 
 
// 数组都继承于Array.prototype  
// (indexOf, forEach等方法都是从它继承而来). 
// 原型链如下: 
// a ---> Array.prototype ---> Object.prototype ---> null 
 
function f(){ 
 return 2; 
} 
 
// 函数都继承于Function.prototype 
// (call, bind等方法都是从它继承而来): 
// f ---> Function.prototype ---> Object.prototype ---> null

2.使用构造器创建对象

在 JavaScript 中,构造器其实就是一个普通的函数。当使用 new 操作符 来作用这个函数时,它就可以被称为构造方法(构造函数)。

function Graph() { 
 this.vertexes = []; 
 this.edges = []; 
} 
 
Graph.prototype = { 
 addVertex: function(v){ 
  this.vertexes.push(v); 
 } 
}; 
 
var g = new Graph(); 
// g是生成的对象,他的自身属性有'vertices'和'edges'. 
// 在g被实例化时,g.[[Prototype]]指向了Graph.prototype.

3.使用 Object.create 创建对象

ECMAScript 5 中引入了一个新方法:Object.create()。可以调用这个方法来创建一个新对象。新对象的原型就是调用 create 方法时传入的第一个参数:

var a = {a: 1};  
// a ---> Object.prototype ---> null 
 
var b = Object.create(a); 
// b ---> a ---> Object.prototype ---> null 
console.log(b.a); // 1 (继承而来) 
 
var c = Object.create(b); 
// c ---> b ---> a ---> Object.prototype ---> null 
 
var d = Object.create(null); 
// d ---> null 
console.log(d.hasOwnProperty); // undefined, 因为d没有继承Object.prototype

4.使用 class 关键字

ECMAScript6 引入了一套新的关键字用来实现 class。使用基于类语言的开发人员会对这些结构感到熟悉,但它们是不一样的。 JavaScript 仍然是基于原型的。这些新的关键字包括 class, constructor,static, extends, 和 super.

"use strict"; 
 
class Polygon { 
 constructor(height, width) { 
  this.height = height; 
  this.width = width; 
 } 
} 
 
class Square extends Polygon { 
 constructor(sideLength) { 
  super(sideLength, sideLength); 
 } 
 get area() { 
  return this.height * this.width; 
 } 
 set sideLength(newLength) { 
  this.height = newLength; 
  this.width = newLength; 
 } 
} 
 
var square = new Square(2);

5.性能

在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。另外,试图访问不存在的属性时会遍历整个原型链。遍历对象的属性时,原型链上的每个可枚举属性都会被枚举出来。

检测对象的属性是定义在自身上还是在原型链上,有必要使用 hasOwnProperty 方法,所有继承自Object.proptotype 的对象都包含这个方法。

hasOwnProperty 是 JavaScript 中唯一一个只涉及对象自身属性而不会遍历原型链的方法。

注意:仅仅通过判断值是否为 undefined 还不足以检测一个属性是否存在,一个属性可能存在而其值恰好为undefined。

6.不好的实践:扩展原生对象的原型

一个经常被用到的错误实践是去扩展 Object.prototype 或者其他内置对象的原型。该技术被称为 monkey patching,它破坏了原型链的密封性。尽管,一些流行的框架(如 Prototype.js)在使用该技术,但是并没有足够好的理由要用其他非标准的方法将内置的类型系统搞乱。我们去扩展内置对象原型的唯一理由是引入新的 JavaScript 引擎的某些新特性,比如Array.forEach。

示例EDIT

B 将继承自 A:

function A(a){ 
 this.varA = a; 
} 
 
// 以上函数 A 的定义中,既然 A.prototype.varA 总是会被 this.varA 遮蔽, 
// 那么将 varA 加入到原型(prototype)中的目的是什么? 
A.prototype = { 
 varA : null, // 既然它没有任何作用,干嘛不将 varA 从原型(prototype)去掉? 
   // 也许作为一种在隐藏类中优化分配空间的考虑? 
   // https://developers.google.com/speed/articles/optimizing-javascript#Initializing instance variables 
   // 将会验证如果 varA 在每个实例不被特别初始化会是什么情况。 
 doSomething : function(){ 
  // ... 
 } 
} 
 
function B(a, b){ 
 A.call(this, a); 
 this.varB = b; 
} 
B.prototype = Object.create(A.prototype, { 
 varB : { 
  value: null,  
  enumerable: true,  
  configurable: true,  
  writable: true  
 }, 
 doSomething : {  
  value: function(){ // override 
   A.prototype.doSomething.apply(this, arguments); // call super 
   // ... 
  }, 
  enumerable: true, 
  configurable: true,  
  writable: true 
 } 
}); 
B.prototype.constructor = B; 
 
var b = new B(); 
b.doSomething();

最重要的部分是:

  • 类型被定义在 .prototype 中
  • 而你用 Object.create() 来继承

三、prototype 和 Object.getPrototypeOf

对于从 Java 或 C++ 转过来的开发人员来说 JavaScript 会有点让人困惑,因为它全部都是动态的,都是运行时,而且不存在类(classes)。所有的都是实例(对象)。即使我们模拟出的 “类(classes)”,也只是一个函数对象。

你可能已经注意到,我们的函数 A 有一个特殊的属性叫做原型。这个特殊的属性与 JavaScript 的 new 运算符一起工作。对原型对象的引用会复制到新实例内部的 [[Prototype]] 属性。例如,当你这样: var a1 = new A(), JavaScript 就会设置:a1.[[Prototype]] = A.prototype(在内存中创建对象后,并在运行 this 绑定的函数 A()之前)。然后在你访问实例的属性时,JavaScript 首先检查它们是否直接存在于该对象中(即是否是该对象的自身属性),如果不是,它会在 [[Prototype]] 中查找。也就是说,你在原型中定义的元素将被所有实例共享,甚至可以在稍后对原型进行修改,这种变更将影响到所有现存实例。

像上面的例子中,如果你执行 var a1 = new A(); var a2 = new A(); 那么 a1.doSomething 事实上会指向Object.getPrototypeOf(a1).doSomething,它就是你在 A.prototype.doSomething 中定义的内容。比如:Object.getPrototypeOf(a1).doSomething == Object.getPrototypeOf(a2).doSomething == A.prototype.doSomething。

简而言之, prototype 是用于类型的,而 Object.getPrototypeOf() 是用于实例的(instances),两者功能一致。

[[Prototype]] 看起来就像递归引用, 如a1.doSomething,Object.getPrototypeOf(a1).doSomething,Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething 等等等, 直到它找到 doSomething 这个属性或者 Object.getPrototypeOf 返回 null。

因此,当你执行:

var o = new Foo();

JavaScript 实际上执行的是:

var o = new Object(); 
o.[[Prototype]] = Foo.prototype; 
Foo.call(o);

(或者类似上面这样的),然后当你执行:

o.someProp;

它会检查是否存在 someProp 属性。如果没有,它会查找Object.getPrototypeOf(o).someProp ,如果仍旧没有,它会继续查找Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp ,一直查找下去,直到它找到这个属性 或者 Object.getPrototypeOf() 返回 null 。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
ExtJS下书写动态生成的xml(兼容火狐)
Apr 02 Javascript
动态改变div的z-index属性的简单实例
Aug 08 Javascript
弹出最简单的模式化遮罩层的js代码
Dec 04 Javascript
Javascript闭包(Closure)详解
May 05 Javascript
javascript实现选中复选框后相关输入框变灰不可用的方法
Aug 11 Javascript
使用微信内置浏览器点击下拉框出现页面乱跳转现象(iphone),该怎么办
Jan 04 Javascript
jquery validate表单验证的基本用法入门
Jan 18 Javascript
jQuery实现图片局部放大镜效果
Mar 17 Javascript
Bootstrap面板使用方法
Jan 16 Javascript
jQuery选取所有复选框被选中的值并用Ajax异步提交数据的实例
Aug 04 jQuery
Vue封装的组件全局注册并引用
Jul 24 Javascript
VUE实现自身整体组件销毁的示例代码
Jan 13 Javascript
Bootstrap CSS组件之输入框组
Dec 17 #Javascript
原生js验证简洁注册登录页面
Dec 17 #Javascript
javascript 数组去重复(在线去重工具)
Dec 17 #Javascript
jQuery Validate验证框架详解(推荐)
Dec 17 #Javascript
Bootstrap CSS组件之导航条(navbar)
Dec 17 #Javascript
Bootstrap CSS组件之导航(nav)
Dec 17 #Javascript
Bootstrap CSS组件之面包屑导航(breadcrumb)
Dec 17 #Javascript
You might like
php数组去重的函数代码
2013/02/03 PHP
PHP实现的一致性Hash算法详解【分布式算法】
2018/03/31 PHP
js小技巧--自动隐藏红叉叉
2007/08/13 Javascript
JS代码放在head和body中的区别分析
2011/12/01 Javascript
jQuery封装的获取Url中的Get参数示例
2013/11/26 Javascript
完美解决IE不支持Data.parse()的问题
2016/11/24 Javascript
ajax级联菜单实现方法实例分析
2016/11/28 Javascript
jQuery实现表格元素动态创建功能
2017/01/09 Javascript
node实现定时发送邮件的示例代码
2017/08/26 Javascript
layer实现关闭弹出层刷新父界面功能详解
2017/11/15 Javascript
python中bisect模块用法实例
2014/09/25 Python
Python获取Windows或Linux主机名称通用函数分享
2014/11/22 Python
Python中的特殊语法:filter、map、reduce、lambda介绍
2015/04/14 Python
Python中用psycopg2模块操作PostgreSQL方法
2017/11/28 Python
浅谈配置OpenCV3 + Python3的简易方法(macOS)
2018/04/02 Python
Tensorflow中使用tfrecord方式读取数据的方法
2018/06/19 Python
Python设计模式之享元模式原理与用法实例分析
2019/01/11 Python
Python设计模式之状态模式原理与用法详解
2019/01/15 Python
python学生信息管理系统实现代码
2019/12/17 Python
python从内存地址上加载python对象过程详解
2020/01/08 Python
python字典与json转换的方法总结
2020/12/28 Python
CSS3解决移动页面上点击链接触发色块的问题
2016/06/03 HTML / CSS
html5+css3实现一款注册表单实例
2013/04/17 HTML / CSS
美国Curacao百货连锁店网站:iCuracao.com
2019/07/20 全球购物
应用心理学个人的求职信
2013/12/08 职场文书
暑期实习鉴定
2013/12/16 职场文书
有多年工作经验的自我评价
2014/03/02 职场文书
领导干部四风问题自我剖析材料
2014/09/25 职场文书
群众对十八届四中全会的期盼
2014/10/17 职场文书
一年级班主任工作总结2014
2014/11/08 职场文书
2014年信贷员工作总结
2014/11/18 职场文书
公证书格式
2015/01/23 职场文书
酒店工程部岗位职责
2015/02/12 职场文书
2016年国庆节新闻稿范文
2015/11/25 职场文书
Golang 对es的操作实例
2022/04/20 Golang
CSS 鼠标点击拖拽效果的实现代码
2022/12/24 HTML / CSS