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 相关文章推荐
Node.js文件操作详解
Aug 16 Javascript
Jquery 实现checkbox全选方法
Jan 28 Javascript
详谈LABJS按需动态加载js文件
May 07 Javascript
Bootstrap学习笔记之js组件(4)
Jun 12 Javascript
javascript this详细介绍
Sep 19 Javascript
浅谈JavaScript的计时器对象
Dec 26 Javascript
Vue单页式应用(Hash模式下)实现微信分享的实例
Jul 21 Javascript
JS实现基于拖拽改变物体大小的方法
Jan 23 Javascript
Vue动态组件与异步组件实例详解
Feb 23 Javascript
原生JS forEach()和map()遍历的区别、兼容写法及jQuery $.each、$.map遍历操作
Feb 27 jQuery
javascript设计模式 ? 简单工厂模式原理与应用实例分析
Apr 09 Javascript
微信小程序学习总结(三)条件、模板、文件引用实例分析
Jun 04 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
约瑟夫环问题的PHP实现 使用PHP数组内部指针操作函数
2010/10/12 PHP
php生成xml时添加CDATA标签的方法
2014/10/17 PHP
THINKPHP在添加数据的时候获取主键id的值方法
2017/04/03 PHP
PHP 进度条函数的简单实例
2017/09/19 PHP
Auntion-TableSort国人写的一个javascript表格排序的东西
2007/11/12 Javascript
JavaScript 弹出窗体点击按钮返回选择数据的实现
2010/04/01 Javascript
Asp.net下利用Jquery Ajax实现用户注册检测(验证用户名是否存)
2010/09/12 Javascript
jQuery学习笔记之DOM对象和jQuery对象
2010/12/22 Javascript
JQuery中extend的用法实例分析
2015/02/08 Javascript
js+HTML5实现canvas多种颜色渐变效果的方法
2015/06/05 Javascript
jquery对象访问是什么及使用方法介绍
2016/05/03 Javascript
AngularJS基础 ng-csp 指令详解
2016/08/01 Javascript
详解js的延迟对象、跨域、模板引擎、弹出层、AJAX【附实例下载】
2016/12/19 Javascript
用jquery的attr方法实现图片切换效果
2017/02/05 Javascript
vue的Virtual Dom实现snabbdom解密
2017/05/03 Javascript
Vue2单一事件管理组件通信
2017/05/09 Javascript
Vue2.0 从零开始_环境搭建操作步骤
2017/06/14 Javascript
JS实现根据指定值删除数组中的元素操作示例
2018/08/02 Javascript
详解Vue项目中实现锚点定位
2019/04/24 Javascript
JS/jQuery实现超简单的Table表格添加,删除行功能示例
2019/07/31 jQuery
Vue数字输入框组件的使用方法
2019/10/19 Javascript
浅谈element中InfiniteScroll按需引入的一点注意事项
2020/06/05 Javascript
实例讲解React 组件生命周期
2020/07/08 Javascript
[02:31]《DAC最前线》之选手酒店现场花絮
2015/01/30 DOTA
Python函数中的函数(闭包)用法实例
2016/03/15 Python
python2.7 mayavi 安装图文教程(推荐)
2017/06/22 Python
Python使用pip安装报错:is not a supported wheel on this platform的解决方法
2018/01/23 Python
VSCode基础使用与VSCode调试python程序入门的图文教程
2020/03/30 Python
python3+openCV 获取图片中文本区域的最小外接矩形实例
2020/06/02 Python
基于python实现删除指定文件类型
2020/07/21 Python
Html5页面中的返回实现的方法
2018/02/26 HTML / CSS
如何利用XMLHTTP检测URL及探测服务器信息
2013/11/10 面试题
2014学年自我鉴定
2014/02/23 职场文书
4S店收银员岗位职责
2015/04/07 职场文书
2015年小学教师培训工作总结
2015/07/21 职场文书
详解JavaScript中的执行上下文及调用堆栈
2021/04/29 Javascript