pdw00 2019-06-21
每个独立的JavaScript对象都是一个属性的集合,独立对象间没有任何关系
ES5中的类是基于原型继承实现的:如果两个对象从同一个原型对象继承属性,称两个对象为同一个类的实例。r instanceof Range.prototype操作符是检查对象r是否继承自Range.prototype
JavaScript中的类可以动态继承
JavaScript中所有类的实例都从同一个原型对象上继承属性。原型对象是函数的prototype属性,每个函数都有。Function.bind()方法返回的函数没有prototype属性。
工厂方法:显式创建一个对象,并将其作为返回值
构造函数用来初始化新创建的对象,每个新创建对象都继承了构造函数的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()原型对象是类的唯一标识:当且仅当两个对象继承自同一个原型对象时,他们才属于同一个类的实例。
r instanceof Range.prototype操作符是检查对象r是否继承自Range.prototype
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 + ")";
};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");
}
};JavaScript中基于原型对象的继承机制是动态的:原型对象的属性发生变化,会影响所有继承该原型对象的实例对象,即使实例对象已经定义。(原理应该是实例对象中只是保存指向原型对象的引用)
不推荐直接在
prototype对象上添加属性或方法,ES5之前不能设置添加的属性和方法为不可枚举,会被for-in循环遍历,ES5中通过Object.defineProperty()方法设置对象属性。
使用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属性和构造函数名称三种方式可以区分自定义类型,但各自与各自的缺点
instanceof操作符如果对象o继承自对象c.prototype,o instanceof c返回true,缺点是不能返回类名称,只能检测对象是否属于某个类。其中c.prototype可以是原型链上的对象
使用c.prototype.isPrototypeOf(o)方法可以检测o继承的原型链上是否有原型对象c.prototype。
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; // 自定义类型
}
};在多个执行上下文中都存在构造器函数的副本时,instanceof操作符与constructor属性检测结果会出错,但是构造器函数本身的名称没有改变,可以作为标识
可以向鸭子一样走路、游泳并且嘎嘎叫的鸟就是鸭子。
以部分特征属性来描述一类对象(关注对象能做什么,弱化对象的类型)
许多OO语言支持接口继承与实现继承。但是ECMAScript没有函数签名,只支持实现继承,继承的实现主要依赖于原型链
原型链式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}所有引用类型都继承自object,函数的默认原型是object的实例,默认原型内包含指向Object.prototype的引用,这是所有自定义类型都会继承toString()、valueOf()等方法的根本原因
Object.prototype没有原型,其原型为null,即
Object.prototype.__proto__ === null; // true
Object.prototype.__proto__是原型链的末端,出口

使用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
如果在子类中定义新方法或者重写父类的方法,必须子类替换原型语句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对于包含引用类型值的原型对象:所有势力共享原型的属性,如果其属性值是引用类型:在一个实例上修改该引用类型的值,会体现在所有的实例对象上 。-----所以需要将引用类型值定义在构造函数中,而非原型对象中。
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点原因,很少单独使用原型链
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如果仅仅使用借用构造函数模式,只能讲方法都定义在构造函数中,不能复用函数,所以借用构造函数模式很少单独使用
将原型链模式与借用构造函数模式组合,发挥二者的长处。其思路:
使用原型链实现原型属性和方法的继承;通过借用构造函数实现实例属性继承。组合继承避免原型链与借用构造函数的缺点,融合优点,是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']ECMAScript中创建对象的模式:
工厂模式
构造函数模式
原型模式
ECMAScript中主要的继承模式是组合继承:综合原型链模式与借用构造函数模式的优点。
是一道经常出现在前端面试时的问题。如果只是简单的了解new关键字是实例化构造函数获取对象,是万万不能够的。更深入的层级发生了什么呢?同时面试官想从这道题里面考察什么呢?下面胡哥为各位小伙伴一一来解密。