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 相关文章推荐
WebGame《逆转裁判》完整版 代码下载(1月24日更新)
Jan 29 Javascript
JS如何将UTC格式时间转本地格式
Sep 04 Javascript
JQ获取动态加载的图片大小的正确方法分享
Nov 08 Javascript
javascript获取鼠标点击元素对象(示例代码)
Dec 20 Javascript
json中换行符的处理方法示例介绍
Jun 10 Javascript
jquery+css实现动感的图片切换效果
Nov 25 Javascript
js实现淡入淡出轮播切换功能
Jan 13 Javascript
vue使用axios跨域请求数据问题详解
Oct 18 Javascript
对angularJs中2种自定义服务的实例讲解
Sep 30 Javascript
微信小程序学习笔记之文件上传、下载操作图文详解
Mar 29 Javascript
微信公众号平台接口开发 获取微信服务器IP地址方法解析
Aug 14 Javascript
Vue 动态路由的实现及 Springsecurity 按钮级别的权限控制
Sep 05 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
比特率,大家看看这个就不用收音机音质去比MP3音质了
2021/03/01 无线电
PHP使用PHPMailer发送邮件的简单使用方法
2013/11/12 PHP
Nginx服务器上安装并配置PHPMyAdmin的教程
2015/08/18 PHP
php生成酷炫的四个字符验证码
2016/04/22 PHP
php微信开发之上传临时素材
2016/06/24 PHP
JS无限树状列表实现代码
2011/01/11 Javascript
jquery数组之存放checkbox全选值示例代码
2013/12/20 Javascript
异步安全加载javascript文件的方法
2015/07/21 Javascript
JSONP和批量操作功能的实现方法
2016/08/21 Javascript
用v-html解决Vue.js渲染中html标签不被解析的问题
2016/12/14 Javascript
基于JavaScript实现下拉列表左右移动代码
2017/02/07 Javascript
详解vue2路由vue-router配置(懒加载)
2017/04/08 Javascript
jQuery遍历节点方法汇总(推荐)
2017/05/13 jQuery
解决ie img标签内存泄漏的问题
2017/10/13 Javascript
vue中手机号,邮箱正则验证以及60s发送验证码的实例
2018/03/16 Javascript
js实现倒计时秒杀效果
2020/03/25 Javascript
jquery实现轮播图特效
2020/04/12 jQuery
uni-app使用微信小程序云函数的步骤示例
2020/05/22 Javascript
Python版实现微信公众号扫码登陆
2020/05/28 Javascript
python实现将一个数组逆序输出的方法
2018/06/25 Python
Python爬虫常用小技巧之设置代理IP
2018/09/13 Python
Python读取YUV文件,并显示的方法
2018/12/04 Python
python输出pdf文档的实例
2020/02/13 Python
Pytorch框架实现mnist手写库识别(与tensorflow对比)
2020/07/20 Python
YSL Beauty加拿大官方商城:圣罗兰美妆加拿大
2017/05/15 全球购物
天网面试题
2013/04/07 面试题
JDBC操作数据库的基本流程是什么
2014/10/28 面试题
医生实习工作总结的自我评价
2013/09/27 职场文书
中学生期末评语
2014/02/03 职场文书
企业消防安全责任书
2014/07/23 职场文书
支部书记四风问题对照检查材料
2014/10/04 职场文书
大学生考试作弊检讨书1000字
2014/10/14 职场文书
预防艾滋病宣传活动总结
2015/05/09 职场文书
2016年党员学习廉政准则心得体会
2016/01/20 职场文书
2016年公务员六五普法心得体会
2016/01/21 职场文书
Vscode中SSH插件如何远程连接Linux
2022/05/02 Servers