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 打印内容方法小结
Nov 04 Javascript
jquery 多行文本框(textarea)高度变化
Jul 03 Javascript
jQuery学习笔记之toArray()
Jun 09 Javascript
node.js中的fs.close方法使用说明
Dec 17 Javascript
JS中Location使用详解
May 12 Javascript
给before和after伪元素设置js效果的方法
Dec 04 Javascript
jquery实现全屏滚动
Dec 28 Javascript
Node中使用ES6语法的基础教程
Jan 05 Javascript
浅析Angular19 自定义表单控件
Jan 31 Javascript
Vue利用Blob下载原生二进制数组文件
Sep 25 Javascript
Vue 中如何将函数作为 props 传递给组件的实现代码
May 12 Javascript
js实现菜单跳转效果
Dec 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
一个很方便的 XML 类!!原创的噢
2006/10/09 PHP
定义php常量的详解
2013/06/09 PHP
PHP-Fcgi下PHP的执行时间设置方法
2013/08/02 PHP
PHP+mysql+ajax轻量级聊天室实现方法详解
2016/10/17 PHP
js调用浏览器打印模块实现点击按钮触发自定义函数
2014/03/21 Javascript
javascript实现下班倒计时效果的方法(可桌面通知)
2015/07/10 Javascript
JavaScript学习笔记之DOM基础 2.4
2015/08/14 Javascript
利用JS生成博文目录及CSS定制博客
2016/02/10 Javascript
AngularJS入门教程之表格实例详解
2016/07/27 Javascript
js 获取元素所有兄弟节点的实现方法
2016/09/06 Javascript
node.js路径处理方法以及绝对路径详解
2021/03/04 Javascript
前端js弹出框组件使用方法
2020/08/24 Javascript
jQuery实现立体式数字动态增加(animate方法)
2016/12/21 Javascript
Vue.js鼠标悬浮更换图片功能
2017/05/17 Javascript
layui前段框架日期控件使用方法详解
2017/05/19 Javascript
解决AjaxFileupload 上传时会出现连接重置的问题
2017/07/07 Javascript
基于VUE移动音乐WEBAPP跨域请求失败的解决方法
2018/01/16 Javascript
php结合js实现多条件组合查询
2019/05/28 Javascript
vant IndexBar实现的城市列表的示例代码
2019/11/20 Javascript
微信小程序实现吸顶特效
2020/01/08 Javascript
[01:08:29]DOTA2-DPC中国联赛定级赛 RNG vs Aster BO3第一场 1月9日
2021/03/11 DOTA
Python中的数据对象持久化存储模块pickle的使用示例
2016/03/03 Python
Python 实现王者荣耀中的敏感词过滤示例
2019/01/21 Python
python 定时器每天就执行一次的实现代码
2019/08/14 Python
如何使用python传入不确定个数参数
2020/02/18 Python
python 三种方法提取pdf中的图片
2021/02/07 Python
深圳茁壮笔试题
2015/05/28 面试题
实习生个人的自我评价
2013/12/08 职场文书
建筑总经理岗位职责
2014/02/02 职场文书
财务情况说明书范文
2014/05/06 职场文书
党员教师四风问题对照检查材料
2014/09/26 职场文书
汽车销售员岗位职责
2015/04/11 职场文书
2015年语言文字工作总结
2015/07/23 职场文书
React四级菜单的实现
2022/04/08 Javascript
Nginx配置根据url参数重定向
2022/04/11 Servers
vue el-table实现递归嵌套的示例代码
2022/08/14 Vue.js