资讯专栏INFORMATION COLUMN

JavaScript || 类和模块

CoorChice / 2177人阅读

摘要:属性每个函数默认有属性方法返回的函数除外,其值为构造函数创建对象继承的对象。其思路使用原型链实现原型属性和方法的继承通过借用构造函数实现实例属性继承。

1 类和模块

每个独立的JavaScript对象都是一个属性的集合,独立对象间没有任何关系

ES5中的类是基于原型继承实现的:如果两个对象从同一个原型对象继承属性,称两个对象为同一个类的实例。r instanceof Range.prototype操作符是检查对象r是否继承自Range.prototype

JavaScript中的类可以动态继承

1.1 类和原型

JavaScript中所有类的实例都从同一个原型对象上继承属性。原型对象是函数的prototype属性,每个函数都有。Function.bind()方法返回的函数没有prototype属性

工厂方法:显式创建一个对象,并将其作为返回值

1.2 类和构造函数

构造函数用来初始化新创建的对象,每个新创建对象都继承了构造函数的prototype属性指向的原型对象。

关于构造函数的约定:

构造函数的首字母大写;

构造函数必须通过new关键字调用才能创建对象,否则与普通函数无异;

原型对象必须通过Range.prototype引用

通过new关键字调用构造函数时,先创建一个空对象,将构造函数的this绑定到该对象;然后利用构造函数初始化该对象

function Range(from, to) {
  this.from = from;
  this.to = to;
}
// 新创建的所有对象都继承这个原型对象
Range.prototype = {   // 重置原型对象的constructor属性
  // 判断x是否在范围之内
  includes: function(x) {return this.from <= x && x <= this.to;},
  // 对于范围内的整数调用一次f方法
  foreach: function(f) {
    for(var x=Math.ceil(this.from); x<=this.to; x++) {
        f(x);
    }
  },
  toString: function() {return "(" + this.from + "..." + this.to + ")";}
};

var r = new Range(1, 3);
console.log(r instanceof Range);  // true
r.foreach(console.log);   // 1 2 3
console.log(Range.prototype.constructor); // 原型对象的constructor属性被重置,不再指向Range()
1.3 构造函数和类的标识

原型对象是类的唯一标识:当且仅当两个对象继承自同一个原型对象时,他们才属于同一个类的实例。

r instanceof Range.prototype操作符是检查对象r是否继承自Range.prototype

1.4 constructor属性

原型对象中的constructor属性是构造函数的引用,但如果直接用字面量对象重写Range.prototype,新对象中没有constructor属性,会默认指向Object()构造函数。

重置constructor属性指向的方法:

// 重置constructor属性的方法:
// 1 显式为原型添加一个构造函数属性
Range.prototype = {
  constructor: Range,   // 显式增加指向Range的constructor属性
  includes: function(x) {return this.from <= x && x <= this.to;},
  foreach: function(f) {
    for(var x=Math.ceil(this.from); x<=this.to; x++) {
        f(x);
    }
  },
  toString: function() {return "(" + this.from + "..." + this.to + ")";}
};

// 2 依次为原型对象添加方法
Range.prototype.includes = function(x) {
  return this.from <= x && x <= this.to;
};
Range.prototype.foreach = function(a) {
  for(var a=Math.ceil(this.from); a<=this.to; a++) {
      f(a);
    }
};
Range.prototype.toString = function(x) {
  return "(" + this.from + "..." + this.to + ")";
};
2 类的补充 2.1 JavaScript中的函数

JavaScript中类中的函数以值的形式出现,如果一个属性值是函数,称其为方法。

类的三种对象:

构造函数对象:定义类名,任何添加到构造函数对象本身的属性都是类字段或类方法

原型对象:原型对象的所有属性都被实例对象继承。

实例对象:类的每个实例对象都是独立对象,直接为每个实例对象定义的属性不会被其他实例共享。实例方法与属性

/*
 * Complex用于描述复数类
 * 复数是实数与虚数之和,虚数i的平方为-1
 */
function Complex(real, imaginary) {
    if(isNaN(real) || isNaN(imaginary)) {   // 确保两个参数都是数字
        throw new TypeError();
    }
    this.r = real;
    this.i = imaginary;
}
// 两个复数对象之和为一个新的复数对象,使用this代表当前复数对象
Complex.prototype.add = function(that) {
    return new Complex(this.r + that.r, this.i + that.i); 
};

Complex.prototype.multiply = function(that) {
    return new Complex(this.r * that.r - this.i * that.i, this.r * that.i + this.i * that.r);
};
// 复数对象的模:原点(0, 0)到复平面的距离
Complex.prototype.mag = function() {
    return Math.sqrt(this.r * this.r + this.i * this.i)
};
// 复数求负运算
Complex.prototype.neg = function() {
    return new Complex(-this.r, -this.i);
};
// 将复数转化为字符串
Complex.prototype.toString = function() {
    return "{" + this.r + "," + this.i + "}";
};
// 当前复数对象是否与另外一个复数对象值相等
Complex.prototype.equal = function(that) {
    return that != null && that.constructor === Complex && this.r === that. r && this.i === that.i;
};

// 类属性
Complex.ZERO = new Complex(0, 0);
Complex.ONE = new Complex(1, 0);
Complex.I = new Complex(0, 1);

// 类方法:将实例对象toString()方法返回的字符串解析为一个Complex对象
// 或抛出类型错误异常
Complex._format = /^{([^,]+),([^}]+)}$/;
Complex.parse = function(s) {
    try {  // 假设解析成功
        var m = Complex._format.exec(s);
        return new Complex(parseFloat(m[1]), parseFloat(m[2]);
    } catch(e) {
        throw new TypeError("can"t parse " + s + "as a complex number");
    }
};

2.2 类的扩充

JavaScript中基于原型对象的继承机制是动态的:原型对象的属性发生变化,会影响所有继承该原型对象的实例对象,即使实例对象已经定义。(原理应该是实例对象中只是保存指向原型对象的引用

不推荐直接在prototype对象上添加属性或方法,ES5之前不能设置添加的属性和方法为不可枚举,会被for-in循环遍历,ES5中通过Object.defineProperty()方法设置对象属性。

2.3 类和类型

使用typeof操作符可以区分基本数据类型:undefinednullnumberstringfunctionobjectboolean。要区分数组,有两种方法:

ES5中的Array.isArray()方法

typeof o === "object" && Object.prototype.toString.call(o).slice(8, -1) === "Array"

区分自定义类型

使用typeof操作符并不能区分自定义类型:instanceof操作符、constructor属性和构造函数名称三种方式可以区分自定义类型,但各自与各自的缺点

1 instanceof操作符

如果对象o继承自对象c.prototypeo instanceof c返回true,缺点是不能返回类名称,只能检测对象是否属于某个类。其中c.prototype可以是原型链上的对象

使用c.prototype.isPrototypeOf(o)方法可以检测o继承的原型链上是否有原型对象c.prototype

2 constructor属性

每个函数默认有prototype属性(bind()方法返回的函数除外),其值为构造函数创建对象继承的对象。原型对象constructor属性指向构造函数。

缺点是并非所有对象都带有constructor属性。

function typeAndValue(x) {
    if(x == null || x == undefined) {
        return "";  //null和undefined没有构造函数
    }
    switch (x.constructor) {
        case Number: return "Number: " + x;  // 原始类型
        case String: return "String: " + x;
        case Date: return "Date: " + x;      // 内置类型
        case RegExp: return "RegExp: " + x;
        case Complex: return "Complex: " + x;  // 自定义类型
    }
};
3 构造器函数的名称

在多个执行上下文中都存在构造器函数的副本时,instanceof操作符与constructor属性检测结果会出错,但是构造器函数本身的名称没有改变,可以作为标识

4 鸭子类型

可以向鸭子一样走路、游泳并且嘎嘎叫的鸟就是鸭子。

以部分特征属性来描述一类对象(关注对象能做什么,弱化对象的类型)

3 继承

许多OO语言支持接口继承与实现继承。但是ECMAScript没有函数签名,只支持实现继承,继承的实现主要依赖于原型链

3.1 原型链

原型链式ECMAScript实现继承的主要方法:子类的原型对象是父类的实例对象。

构造函数、原型与实例的关系:

构造函数的prototype属性指向原型对象;

原型对象的constructor属性指向构造函数;

实例的__proto__属性指向原型对象,实例与构造函数没有直接联系

SubType的原型重写为SuperType的实例对象,新原型对象作为SuperType一个实例拥有全部属性和方法,内部__proto__属性指向SuperType的原型。

instance指向SubType的原型,SubType的原型指向SuperType的原型。形成一条原型链:原型链的搜索机制。先搜索实例对象instance,再搜索Subype的原型,再搜索SuperType的原型,依次向上

// 父类
function SuperType() {
    this.property = true;    
}
SuperType.prototype.getSuperValue = function() {
    return this.property;
}

// 子类
function SubType() {
    this.subProperty = false;
}
// 子类的原型对象是父类的实例对象(其__proto__属性指向父类的原型对象)
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function() {
    return this.subProperty;
}

var instance = new SubType();
instance.getSuperValue();   // true,子类调用父类的方法

注:实例对象instance的原型的构造函数不是SubType,而是SuperType。因为重置SubType.prototype的指向,但是没有重置construtor的指向

console.log(instance.__proto__.constructor);  // function SuperType() {native code}
1原型链末端

所有引用类型都继承自Object函数的默认原型是Object的实例,默认原型内包含指向Object.prototype的引用,这是所有自定义类型都会继承toString()valueOf()等方法的根本原因

Object.prototype没有原型,其原型为null,即

Object.prototype.__proto__ === null;  // true

Object.prototype.__proto__是原型链的末端,出口

2原型与实例的关系

使用instanceof操作符与isPrototypeOf()方法:

instance instanceof Object;    // true
instance instanceof SuperType;    // true
instance instanceof SubType;    // true

Object.prototype.isPrototypeOf(instance);   // true
SuperType.prototype.isPrototypeOf(instance);   // true
SubType.prototype.isPrototypeOf(instance);   // true
3 谨慎定义子类中方法的位置

如果在子类中定义新方法或者重写父类的方法,必须子类替换原型语句SubType.prototype = new SuperType();之后,否则不起作用

function SuperType() {
    this.property = true;    
}
SuperType.prototype.getSuperValue = function() {
    return this.property;
}

// 子类
function SubType() {
    this.subProperty = false;
}
// 子类的原型对象是父类的实例对象(其__proto__属性指向父类的原型对象)
SubType.prototype = new SuperType();

// 添加新方法
SubType.prototype.getSubValue = function() {
    return this.subProperty;
}
// 重写父类中的方法
SuperType.prototype.getSuperValue = function() {
    return false;
}

var instance = new SubType();
instance.getSuperValue();   // false

在使用原型链实现继承时,不能使用字面量方式创建原型对象,否则会切断原型链,将原型对象重行指向字面量对象

function SuperType() {
    this.property = true;    
}
SuperType.prototype.getSuperValue = function() {
    return this.property;
}

// 子类
function SubType() {
    this.subProperty = false;
}
// 子类的原型对象是父类的实例对象(其__proto__属性指向父类的原型对象)
SubType.prototype = new SuperType();

// 使用字面量方式添加新方法,使上一行代码无效
SubType.prototype = {
    getSubValue: function() {
        return this.subProperty;
    },
    getSuperValue: function() {
        return false;
    }
}

var instance = new SubType();
instance.getSuperValue();   // false
4 原型链的问题

对于包含引用类型值的原型对象:所有势力共享原型的属性,如果其属性值是引用类型:在一个实例上修改该引用类型的值,会体现在所有的实例对象上 。-----所以需要将引用类型值定义在构造函数中,而非原型对象中。

function SuperColor() {
    this.color = ["red", "blue"];   
}

function SubColor() {

}
SubColor.prototype = new SuperColor();   // 子类原型定义为父类的实例,但是color属性值为引用类型

var col1 = new SubColor();
col1.color.push("green");
console.log(col1.color);   // ["red", "blue", "green"]

// 注意,所有的实例对象的color都改变
var col2 = new SubColor();
console.log(col2.color);  // ["red", "blue", "green"]

没有办法在不影响所有对象实例的情况下,向父类的构造函数传递参数。

基于上述2点原因,很少多带带使用原型链

3.2 借用构造函数constructor stealing

在子类中,利用创建的对象,以方法的形式调用父类构造器函数,父类构造器函数仅用于初始化子类中创建的对象

基本思想:在子类构造函数内部调用父类构造函数。因为函数只是特定环境中执行代码的对象,可以使用call()apply()方法在新创建对象上执行构造函数

1.通过new调用SubColor():本质先创建一个对象,将其绑定到this再利用this调用函数SuperColor(),设置this.color属性值

2.每次调用new SubColor()创建的都是独立的对象,所以不影响

function SuperColor() {
    this.color = ["red", "blue"];   
}

function SubColor() {
    // 继承SuperColor
    // 使用新创建的对象this来调用SuperColor()函数,设置this.color属性值
    // 每次调用new SubColor()创建的都是独立的对象,所以不影响
    SuperColor.call(this);  
}

var col3 = new SubColor();
col1.color.push("green");
console.log(col3.color);   // ["red", "blue", "green"]

var col4 = new SubColor();
console.log(col4.color);  // ["red", "blue"]
传递参数

通过借用构造器函数可以向父类构造函数传递参数。将参数挂载在call()apply()方法中:将父类构造器哈数仅用作初始化对象用

function SuperType(name) {
    this.name = name;
}
function SubType() {
    // 继承SuperType,同时传递参数"Tracy"
    SuperType.call(this, "Tracy");

    this.age = 23; // 实例属性
}

var kyxy = new SubType();
console.log(kyxy.name);   // "Tracy"
console.log(kyxy.age);    //  23
借用构造器函数的问题

如果仅仅使用借用构造函数模式,只能讲方法都定义在构造函数中,不能复用函数,所以借用构造函数模式很少多带带使用

3.3 组合继承

将原型链模式与借用构造函数模式组合,发挥二者的长处。其思路:

使用原型链实现原型属性和方法的继承;通过借用构造函数实现实例属性继承。组合继承避免原型链与借用构造函数的缺点,融合优点,是ECMAScript中最常用的的继承模式

首先使用借用构造函数模式继承实例属性

再使用原型链模式继承原型的属性与方法

借用构造函数模式:利用子类创建空对象,将父类的实例属性拷贝到子类中。因为每个子类是独立的对象,所以享有父类的拷贝也是相互独立的

实例间共享的原型对象中的属性依然通过原型链模式实现

function SuperType(name) {
    this.name = name;
    this.color = ["red", "blue"];
}
SuperType.prototype.sayName = function() {
    coonsle.log(this.name);
}

function SubType(name, age) {
    // 实例属性的继承(不再是引用,而是多带带一份拷贝)
    SuperType.call(this, name);

    this.age = age;
}
// 原型属性与方法的继承
SubType.prototype = new SuperType();
// 重置原型对象constructor的指向
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
    console.log(this.age);
}

var p1 = new SubType("Kyxy", 23);
p1.color.push("black");
p1.sayAge();   // 23
p1.sayName();   // "Kyxy"
p1.color;     // ["red", "blue", "black"]

var p2 = new SubType("Tracy", 23);
p2.color;   // ["red", "blue"]

3.4 总结

ECMAScript中创建对象的模式:

工厂模式

构造函数模式

原型模式

ECMAScript中主要的继承模式是组合继承:综合原型链模式与借用构造函数模式的优点。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/81989.html

相关文章

  • erget源码分析(2):全局哈希基类和全局异步函数对象接口

    摘要:异步函数对象接口,包含和两个成员方法。哈希计数在整个的源码中都没有找到和方法的调用,这两个方法的具体作用是在原生中实现类式继承和私有属性一类的功能。 文件结构 utils/HashObject.ts文件:showImg(https://segmentfault.com/img/bVZpuq?w=642&h=472); 首先解释一下文件结构图 __extends方法 通过原型对象模拟类...

    godlong_X 评论0 收藏0
  • [翻]ECMAScript 6 特性速览

    摘要:类总所周知,不像其他面向对象语言那样支持类,但是可以通过函数和原型来模拟类。如果你学习过或者其他面向对象语言的话,你会觉得很熟悉。结论下一个版本的会带来一个更加简单更加友好的语法来帮助那些从面向对象语言转过来的开发者的学习。 原文地址:http://www.frontendjournal.com/javascript-es6-learn-important-features-in-a-...

    CoderStudy 评论0 收藏0
  • 聊聊毕业设计系列 --- 项目介绍

    摘要:又将整个文艺类阅读系统的业务划分为两大部分,分别是面向管理员和合作作者的后台管理系统和面向用户的移动端,系统的需求分析将围绕这两部分进行展开。 效果展示 showImg(https://user-gold-cdn.xitu.io/2018/8/26/16576a709bd02f5f?w=1409&h=521&f=gif&s=30128195); showImg(https://user...

    Pink 评论0 收藏0
  • 聊聊毕业设计系列 --- 项目介绍

    摘要:又将整个文艺类阅读系统的业务划分为两大部分,分别是面向管理员和合作作者的后台管理系统和面向用户的移动端,系统的需求分析将围绕这两部分进行展开。 效果展示 showImg(https://user-gold-cdn.xitu.io/2018/8/26/16576a709bd02f5f?w=1409&h=521&f=gif&s=30128195); showImg(https://user...

    villainhr 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<