JavaScript || 类和模块

pdw00 2019-06-21

1 类和模块

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

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

  3. 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中类中的函数以值的形式出现,如果一个属性值是函数,称其为方法。

类的三种对象:

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

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

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

    /*
     * 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。要区分数组,有两种方法:

  1. ES5中的Array.isArray()方法

  2. 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实现继承的主要方法:子类的原型对象是父类的实例对象。

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

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

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

  3. 实例的__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,子类调用父类的方法

JavaScript  || 类和模块

注:实例对象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__是原型链的末端,出口

JavaScript  || 类和模块

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 原型链的问题

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

    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. 没有办法在不影响所有对象实例的情况下,向父类的构造函数传递参数。

基于上述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中最常用的的继承模式

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

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

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

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

    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中主要的继承模式是组合继承:综合原型链模式与借用构造函数模式的优点。

相关推荐