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 相关文章推荐
javascript globalStorage类代码
Jun 04 Javascript
js继承的实现代码
Aug 05 Javascript
JavaScript实现页面滚动图片加载(仿lazyload效果)
Jul 22 Javascript
jQuery实现的类似淘宝网站搜索框样式代码分享
Aug 24 Javascript
浏览器环境下JavaScript脚本加载与执行探析之动态脚本与Ajax脚本注入
Jan 19 Javascript
jQuery+vue.js实现的九宫格拼图游戏完整实例【附源码下载】
Sep 12 jQuery
React操作真实DOM实现动态吸底部的示例
Oct 23 Javascript
Vue中UI组件库之Vuex与虚拟服务器初识
May 07 Javascript
vue中引入mxGraph的步骤详解
May 17 Javascript
vue实现表格过滤功能
Sep 27 Javascript
在vscode 中设置 vue模板内容的方法
Sep 02 Javascript
vue watch监控对象的简单方法示例
Jan 07 Vue.js
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
《魔兽争霸3》重制版究竟重制了什么?玩家:这么糊弄真的好吗?
2020/05/04 魔兽争霸
咖啡机如何保养和日常清洁?
2021/03/03 冲泡冲煮
PHP 中执行系统外部命令
2006/10/09 PHP
PHP 遍历文件实现代码
2011/05/04 PHP
php调用nginx的mod_zip模块打包ZIP文件
2014/06/11 PHP
php微信公众号开发(3)php实现简单微信文本通讯
2016/12/15 PHP
PHP数据对象映射模式实例分析
2019/03/29 PHP
解决PHPstudy Apache无法启动的问题【亲测有效】
2020/10/30 PHP
JavaScript 利用StringBuffer类提升+=拼接字符串效率
2009/11/24 Javascript
了解jQuery技巧来提高你的代码
2010/01/08 Javascript
基于pthread_create,readlink,getpid等函数的学习与总结
2013/07/17 Javascript
jquery实现多级下拉菜单的实例代码
2013/10/02 Javascript
JS获取浏览器语言动态加载JS文件示例代码
2014/10/31 Javascript
JavaScript中的Truthy和Falsy介绍
2015/01/01 Javascript
canvas实现绘制吃豆鱼效果
2017/01/12 Javascript
webpack学习笔记之代码分割和按需加载的实例详解
2017/07/20 Javascript
使用js实现将后台传入的json数据放在前台显示
2018/08/06 Javascript
原生JS实现动态添加新元素、删除元素方法
2019/05/05 Javascript
django js 实现表格动态标序号的实例代码
2019/07/12 Javascript
vue router-link 默认a标签去除下划线的实现
2020/11/06 Javascript
python获得图片base64编码示例
2014/01/16 Python
PyQt5打开文件对话框QFileDialog实例代码
2018/02/07 Python
python找出完数的方法
2018/11/12 Python
python 获取键盘输入,同时有超时的功能示例
2018/11/13 Python
css和css3弹性盒模型实现元素宽度(高度)自适应
2019/05/15 HTML / CSS
html5 桌面提醒:Notifycations应用介绍
2012/11/27 HTML / CSS
解决Firefox下不支持outerHTML问题代码分享
2014/06/04 HTML / CSS
巴西最大的珠宝连锁店:Vivara
2019/04/18 全球购物
财务经理岗位职责
2013/11/09 职场文书
自荐信格式
2013/12/01 职场文书
小学门卫岗位职责
2013/12/17 职场文书
大学考试作弊检讨书
2014/01/30 职场文书
军训 自我鉴定
2014/02/03 职场文书
2019年教师节:送给所有老师的祝福语
2019/09/05 职场文书
python opencv人脸识别考勤系统的完整源码
2021/04/26 Python
手把手教你用SpringBoot将文件打包成zip存放或导出
2021/06/11 Java/Android