JavaScript继承的特性与实践应用深入详解


Posted in Javascript onDecember 30, 2018

本文详细讲述了JavaScript继承的特性与实践应用。分享给大家供大家参考,具体如下:

继承是代码重用的模式。JavaScript 可以模拟基于类的模式,还支持其它更具表现力的模式。但保持简单通常是最好的策略。

JavaScript 是基于原型的语言,也就是说它可以直接继承其他对象。

1 伪类

JavaScript 的原型不是直接让对象从其他对象继承,而是插入一个多余的间接层:通过构造函数来产生对象。

当一个函数被创建时,Function 构造器产生的函数对象会运行这样类似的代码:

this.prototype = {constructor : this};

新的函数对象新增了一个 prototype 属性,它是一个包含了 constructor 属性且属性值为该新函数的对象。

当采用构造器调用模式,即用 new 去调用一个函数时,它会这样执行:

Function.method('new', function (){
  var that = Object.create(this.prototype);//创建一个继承了构造器函数的原型对象的新对象
  var other = this.apply(that, arguments);//调用构造器函数,绑定 this 到新对象
  return (typeof other === 'object' && other) || that;//如果构造器函数的返回值不是对象,就直接返回这个新对象
});

我们可以定义一个构造器,然后扩充它的原型:

//定义构造器并扩充原型
var Mammal = function (name) {
  this.name = name;
};
Mammal.prototype.get_name = function () {
  return this.name;
};
Mammal.prototype.says = function () {
  return this.saying || '';
};

然后构造实例:

var myMammal = new Mammal('Herb the mammal');
console.log(myMammal.get_name());//Herb the mammal

构造另一个伪类来继承 Mammal(定义构造器函数并替换它的 prototype):

var Cat = function (name) {
  this.name = name;
  this.saying = 'meow';
};
Cat.prototype = new Mammal();

扩充原型:

Cat.prototype.purr = function (n) {
  var i, s = '';
  for (i = 0; i < n; i += 1) {
    if (s) {
      s += '-';
    }
    s += 'r';
  }
  return s;
};
Cat.prototype.get_name = function () {
  return this.says() + ' ' + this.name + ' ' + this.says();
};
var myCat = new Cat('Henrietta');
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow

我们使用 method 方法定义了 inherits 方法,来隐藏上面这些丑陋的细节:

/**
 * 为 Function.prototype 新增 method 方法
 * @param name 方法名称
 * @param func 函数
 * @returns {Function}
 */
Function.prototype.method = function (name, func) {
  if (!this.prototype[name])//没有该方法时,才添加
    this.prototype[name] = func;
  return this;
};
Function.method('inherits', function (Parent) {
  this.prototype = new Parent();
  return this;
});

这两个方法都返回 this,这样我们就可以以级联的方式编程啦O(∩_∩)O~

var Cat = function (name) {
  this.name = name;
  this.saying = 'meow';
}.inherits(Mammal).method('purr', function (n) {
    var i, s = '';
    for (i = 0; i < n; i += 1) {
      if (s) {
        s += '-';
      }
      s += 'r';
    }
    return s;
  }).method('get_name', function () {
    return this.says() + ' ' + this.name + ' ' + this.says();
  });
var myCat = new Cat('Henrietta');
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow

虽然我们有了行为很像“类”的构造器函数,但没有私有环境,所有的属性都是公开的,而且不能访问父类的方法。

如果在调用构造函数时忘记加上 new 前缀,那么 this 就不会被绑定到新对象上,而是被绑定到了全局变量!!!这样我们不但没有扩充新对象,还破坏了全局变量环境。

这是一个严重的语言设计错误!为了降低出现这个问题的概率,所有的构造器函数都约定以首字母大写的形式来命名。这样当我们看到首字母大写的形式的函数,就知道它是构造器函数啦O(∩_∩)O~

当然,更好的策略是根本不使用构造器函数。

2 对象说明符

有时候,构造器需要接受一大堆参数,这很麻烦。所以在编写构造器时,让它接受一个简单的对象说明符会更好:

var myObject = maker({
 first: f,
 middle: m,
 last: l
});

现在这些参数可以按照任意的顺序排列咯,而且构造器还能够聪明地为那些没有传入的参数使用默认值,代码也变得更易阅读啦O(∩_∩)O~

3 原型

基于原型的继承指的是,一个新对象可以继承一个旧对象的属性。首先构造出一个有用的对象,然后就可以构造出更多与那个对象类似的对象。

/**
 * 原型
 */
var myMammal = {
  name: 'Herb the mammal',
  get_name: function () {
    return this.name;
  },
  says: function () {
    return this.saying || '';
  }
};
//创建新实例
var myCat = Object.create(myMammal);
myCat.name = 'Henrietta';
myCat.saying = 'meow';
myCat.purr = function (n) {
  var i, s = '';
  for (i = 0; i < n; i += 1) {
    if (s) {
      s += '-';
    }
    s += 'r';
  }
  return s;
};
myCat.get_name = function () {
  return this.says() + ' ' + this.name + ' ' + this.says();
};
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow

这里用到了 create 方法来创建新的实例:

Object.create = function (o) {
    var F = function () {
    };
    F.prototype = o;
    return new F();
 }

4 函数化

目前为止看到的继承模式的问题是:无法保护隐私,对象的所有属性都是可见的。有一些无知的程序员会使用伪装私有的模式,即给一个需要私有的属性起一个古怪的名字,并希望其他使用代码的程序员假装看不到它们!

其实有更好的方法:应用模块模式。

我们先构造一个生成对象的函数,它有这些步骤:
①. 创建新对象。这有四种方式:
【1】构造一个对象字面量。
【2】调用一个构造器函数。
【3】构造一个已存在对象的新实例。
【4】调用任意一个会返回对象的函数。
②. 定义私有实例变量与方法。
③. 为这个新对象扩充方法,这些方法拥有特权去访问这些参数。
④. 返回这个新对象。

函数化构造器的伪代码如下:

var constructor = function (spec, my){
  var that, 其他私有变量;
  my = my || {};
 //把共享的变量和函数添加到 my 中
 that = 一个新对象
 //添加给 that 的特权方法
 return that;
};

spec 对象包含了需要构造一个新实例的所有信息,它可以被用到到私有变量或者其他函数中。
my 对象为在一个继承链中的构造器提供了共享的容器,如果没有传入,那么会创建一个 my 对象。

创建特权方法的方式是:把函数定义为私有方法,然后再把它们分配给 that:

var methodical = function (){
 ...
};
that.methodical = methodical;

这样分两步定义的好处是:私有的 methodical 不受这个实例被改变的影响。

现在,我们把这个模式应用到 mammal 示例中:

var mammal = function (spec) {
  var that = {};
  that.get_name = function () {
    return spec.name;
  };
  that.says = function () {
    return spec.saying || '';
  };
  return that;
};
var myMammal = mammal({name: 'Herb'});
console.log(myMammal.get_name());//Herb
var cat = function (spec) {
  spec.saying = spec.saying || 'meow';
  var that = mammal(spec);
  that.purr = function (n) {
    var i, s = '';
    for (i = 0; i < n; i += 1) {
      if (s) {
        s += '-';
      }
      s += 'r';
    }
    return s;
  };
  that.get_name = function () {
    return that.says() + ' ' + spec.name + ' ' + that.says();
  };
  return that;
};
var myCat = cat({name: 'Henrietta'});
console.log(myCat.says());//meow
console.log(myCat.purr(5));//r-r-r-r-r
console.log(myCat.get_name());//meow Henrietta meow

函数化模式还能调用父类的方法。这里我们构造一个 superior 方法,它会返回调用某个方法名的函数:

//返回调用某个方法名的函数
Object.method('superior', function (name) {
  var that = this,
    method = that[name];
  return function () {
    return method.apply(that, arguments);
  };
});

现在创建一个 coolcat,它拥有一个可以调用父类方法的 get_name:

var coolcat = function (spec) {
  var that = cat(spec),
    super_get_name = that.superior('get_name');
  that.get_name = function (n) {
    return 'like ' + super_get_name() + ' baby';
  };
  return that;
};
var myCoolCat = coolcat({name: 'Bix'});
console.log(myCoolCat.get_name());//like meow Bix meow baby

函数化模式有很大的灵活性,而且可以更好地实现封装、信息隐藏以及访问父类方法的能力。

如果对象所有的状态都是私有的,那么就称为防伪对象。这个对象的属性可以被替换或删除,但这个对象的状态不受影响。如果用函数化模式来创建对象,并且这个对象的所有方法都不使用 this 或 that,那么这个对象就是持久性的,它不会被入侵。除非存在特权方法,否则不能访问这个持久性对象的内部状态。

5 事件处理函数

可以构造一个能够给任何对象添加简单事件处理特性的函数。这里,我们给这个对象添加一个 on 方法,fire 方法和私有的事件注册对象:

var eventuality = function (that) {
  var registry = {};
  /**
   * 触发事件
   *
   * 使用 'on' 方法注册的事件处理程序将被调用
   * @param 可以是包含事件名称的字符串,或者是一个拥有 type 属性(值为事件名称)的对象。
   */
  that.fire = function (event) {
    var array,
      func,
      handler,
      i,
      type = typeof event === 'string' ? event : event.type;
    //如果这个事件已被注册,则遍历并依序执行
    if (registry.hasOwnProperty(type)) {
      array = registry[type];
      for (i = 0; i < array.length; i += 1) {
        handler = array[i];//处理程序包含一个方法和一组可选的参数
        func = handler.method;
        if (typeof func === 'string') {//如果方法是字符串形式的名称,则寻找它
          func = this[func];
        }
        //调用它。如果处理程序包含参数,则传递过去,否则就传递事件对象
        func.apply(this, handler.parameters || [event]);
      }
    }
    return this;
  };
  /**
   * 注册一个事件
   * @param type
   * @param method
   * @param parameters
   */
  that.on = function (type, method, parameters) {
    var handler = {
      method: method,
      parameters: parameters
    };
    if (registry.hasOwnProperty(type)) {//如果已存在,就新增数组项
      registry[type].push(handler);
    } else {//新增
      registry[type] = [handler];
    }
    return this;
  };
  return that;
};

可以在任何单独对象上调用 eventuality,授予它事件处理方法。也可以在 that 被返回前,在构造函数中调用它:

eventuality(that);

JavaScript 弱类型的特性在此是一个巨大的优势,因为我们无须处理对象继承关系中的类型O(∩_∩)O~

感兴趣的朋友还可以使用本站在线HTML/CSS/JavaScript代码运行工具:http://tools.3water.com/code/HtmlJsRun测试上述代码运行结果。

更多关于JavaScript相关内容还可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

Javascript 相关文章推荐
用JQuery 实现的自定义对话框
Mar 24 Javascript
JavaScript Event学习第九章 鼠标事件
Feb 08 Javascript
javascript 闭包疑问
Dec 30 Javascript
A标签中通过href和onclick传递的this对象实现思路
Apr 19 Javascript
js鼠标经过tab选项卡时实现切换延迟
Mar 24 Javascript
利用JS测试目标网站的打开响应速度
Dec 01 Javascript
JS实现标签滚动切换效果
Dec 25 Javascript
JS实现带动画的回到顶部效果
Dec 28 Javascript
es6新特性之 class 基本用法解析
May 05 Javascript
三种Webpack打包方式(小结)
Sep 19 Javascript
Vue实现一个图片懒加载插件
Mar 11 Javascript
手写Vue弹窗Modal的实现代码
Sep 11 Javascript
JavaScript函数的特性与应用实践深入详解
Dec 30 #Javascript
基于Three.js实现360度全景图片
Dec 30 #Javascript
three.js实现圆柱体
Dec 30 #Javascript
three.js实现炫酷的全景3D重力感应
Dec 30 #Javascript
Three.js实现3D机房效果
Dec 30 #Javascript
JavaScript对象的特性与实践应用深入详解
Dec 30 #Javascript
three.js搭建室内场景教程
Dec 30 #Javascript
You might like
Ajax PHP分页演示
2007/01/02 PHP
php 判断服务器操作系统的类型
2014/02/17 PHP
解决laravel session失效的问题
2019/10/14 PHP
利用javascript实现一些常用软件的下载导航
2009/08/03 Javascript
jQuery拖拽div实现思路
2014/02/19 Javascript
60个很实用的jQuery代码开发技巧收集
2014/12/15 Javascript
JQuery插件ajaxfileupload.js异步上传文件实例
2015/05/19 Javascript
tuzhu_req.js 实现仿百度图片首页效果
2015/08/11 Javascript
JavaScript实现简单获取当前网页网址的方法
2015/11/09 Javascript
JavaScript获取当前cpu使用率的方法
2015/12/15 Javascript
深入理解JavaScript程序中内存泄漏
2016/03/17 Javascript
JS深度拷贝Object Array实例分析
2016/03/31 Javascript
Bootstrap基本样式学习笔记之图片(6)
2016/12/07 Javascript
select标签设置默认选中的选项方法
2018/03/02 Javascript
用vue2.0实现点击选中active其他选项互斥的效果
2018/04/12 Javascript
使用javascript函数编写简单银行取钱存钱流程
2018/05/26 Javascript
webpack常用配置总览(小结)
2019/11/18 Javascript
vue实现移动端input上传视频、音频
2020/08/18 Javascript
了不起的11个JavaScript代码重构最佳实践小结
2021/01/11 Javascript
[12:51]71泪洒现场!是DOTA2让经典重现
2014/03/24 DOTA
[41:08]TNC vs VG 2018国际邀请赛小组赛BO2 第一场 8.16
2018/08/17 DOTA
Python最基本的数据类型以及对元组的介绍
2015/04/14 Python
Python3中的列表,元组,字典,字符串相关知识小结
2017/11/10 Python
Python SVM(支持向量机)实现方法完整示例
2018/06/19 Python
python模拟菜刀反弹shell绕过限制【推荐】
2019/06/25 Python
详解python中index()、find()方法
2019/08/29 Python
简单瞅瞅Python vars()内置函数的实现
2019/09/27 Python
python爬虫爬取幽默笑话网站
2019/10/24 Python
学python需要去培训机构吗
2020/07/01 Python
Python调用百度OCR实现图片文字识别的示例代码
2020/07/17 Python
Python图像处理二值化方法实例汇总
2020/07/24 Python
HTML5之SVG 2D入门13—svg对决canvas及长处和适用场景分析
2013/01/30 HTML / CSS
FOREO官方网站:LUNA露娜洁面仪
2016/11/28 全球购物
查摆问题对照检查材料
2014/08/28 职场文书
物业接待员岗位职责
2015/04/15 职场文书
2015年测量员工作总结
2015/05/23 职场文书