JavaScript || 类和模块
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
操作符可以区分基本数据类型:undefined
、null
、number
、string
、function
、object
和boolean
。要区分数组,有两种方法:
ES5中的
Array.isArray()
方法typeof o === "object" && Object.prototype.toString.call(o).slice(8, -1) === "Array"
区分自定义类型
使用typeof
操作符并不能区分自定义类型:instanceof
操作符、constructor
属性和构造函数名称三种方式可以区分自定义类型,但各自与各自的缺点
1 instanceof
操作符
如果对象o
继承自对象c.prototype
,o 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中主要的继承模式是组合继承:综合原型链模式与借用构造函数模式的优点。
相关推荐
是一道经常出现在前端面试时的问题。如果只是简单的了解new关键字是实例化构造函数获取对象,是万万不能够的。更深入的层级发生了什么呢?同时面试官想从这道题里面考察什么呢?下面胡哥为各位小伙伴一一来解密。