本章是第四章对象相关的内容。
JavaScript虽然是一门弱类型语言,但它同样是一门面向对象的语言,严格来说它是一门基于原型的面向对象的语言。
在学完后,希望掌握下面知识点:
对象:无序属性的集合,其属性值可以包含基本类型值、对象或者函数等。
对象其实就是一组键值对的组合,键表示的是属性名称,值表示的是属性的值。
对象的属性可以分成两种:
数据属性有 4 个描述其行为的特性,而且都是内部值,所以将其放在两对方括号中:
[[Configurable]]
:表示属性能否删除而重新定义,或者是否可以修改为访问器属 性,默认值为true
[[Enumerable]]
:表示属性是否可枚举,可枚举的属性能够通过for...in
循环返回, 默认值为true
[[Writable]]
:表示属性值能否被修改,默认值为true
[[Value]]
:表示属性的真实值,属性的读取和写入均通过此属性完成,默认值为 undefined
比如下面,定义了一个包含name属性的对象person,name属性的[[Configurable]]、[[Enumerable]]、[[Writable]]特性值都为true,[[Value]]特性值为’kingx’
var person = {
name: 'kingx'
};
如果需要修改数据属性默认的特性,必须要使用Object.defineProperty()
函数:
Object.defineProperty(target,property,{
configurable: true,
enumerable: false,
writable: false,
value: 'kingx'
});
target
:表示目标对象property
:表示将要修改的属性访问器属性同样有 4 个特性:
[[Configurable]]
:表示属性能否删除而重新定义,或者是否可以修改为访问器属性,默认值为true
[[Enumerable]]
:表示属性是否可枚举,可枚举的属性能够通过for…in循环返回,默认值为true
[[Get]]
:在读取属性值时调用的函数(一般称为getter()
函数),负责返回有效的值,默认值为undefined
[[Set]]
:在写入属性值时调用的函数(一般称为setter()
函数),负责处理数据,默认值为undefined
如果需要修改访问器属性默认的特性,则必须使用Object.defineProperty()
函数
var person = {
_age: 10
};
Object.defineProperty(person, "age", {
get: function(){
return this._age;
},
set: function(newValue) {
if (newValue > 10) {
this._age = newValue;
console.log('设置成功');
}
}
});
console.log(person.age); // 10
person.age = 9; //没超过10,所以设置失败
console.log(person.age); // 10
person.age = 19; // 设置成功
console.log(person.age); // 19
对象属性的访问方式有两种,第一种是使用点操作符.
,第二种是使用中括号操作符[]
.
访问属性语法为:
ObjectName.propertyName
ObjectName
:对象名称propertyName
:属性名称比如上面的 person.name,就是访问 person 对象的 name 属性值
[]
访问属性语法为:
ObjectName[propertyName]
比如person['name']
var obj = {};
obj.name = '张三';
var myName = 'name';
console.log(obj.myName); // undefined,访问不到对应的属性
console.log(obj[myName]); // 张三
var obj={};
obj.1=1; // 抛出异常,Unexpected number
obj[2]=2;
console.log(obj.1); // 抛出异常,missing ) after argument list
console.log(obj[2]); // 2
var person = {};
person['first name'] ='kingx';
console.log(person['first name']); // kingx
console.log(person.first name);//抛出异常,missing ) after argument list
最简单的基于Object()构造函数和基于对象字面量 2 种方法简单易懂,但在处理需要同时创建若干个属性名相同,而只是属性值不同的对象时会产生很多冗余代码。因此并不推荐用这 2 种方法批量处理。
除了这两种,还额外介绍了其他 5 种方法。
其中第 6 种构造函数和原型混合的模式是最常见的创建自定义类型对象的方式。
通过Object对象的构造函数生成一个实例,然后给它增加需要的各种属性。
// Object()构造函数生成实例
var person = new Object();
// 为实例新增各种属性
person.name = 'kingx';
person.age = 11;
person.getName = function (){
return this.name;
};
person.address = {
name: '北京市',
code: '100000'
};
对象字面量本身就是一系列键值对的组合,每个属性之间通过逗号分隔。
var person = {
name: 'kingx',
age: 11,
getName: function () {
return this.name;
},
address: {
name: '北京市',
code: '100000'
}
};
工厂方法模式是一种比较重要的设计模式,用于创建对象,旨在抽象出创建对象和属性赋值的过程,只对外暴露出需要设置的属性值。
// 工厂方法,对外暴露接收的name、age、address属性值
function createPerson(name, age, address) {
// 内部通过Object()构造函数生成一个对象,并添加各种属性
var o = new Object();
o.name = name;
o.age = age;
o.address = address;
o.getName = function () {
return this.name;
};
// 返回创建的对象
return o;
}
var person = createPerson('kingx', 11, {
name: '北京市',
code: '100000'
});
使用工厂方法可以减少很多重复的代码,但是创建的所有实例都是Object类型,无法更进一步区分具体的类型。
构造函数是通过this为对象添加属性的,属性值类型可以为基本类型、对象或者函数,然后通过new操作符创建对象的实例。
function Person(name, age, address) {
this.name = name;
this.age = age;
this.address = address;
this.getName = function () {
return this.name;
};
}
var person = new Person('kingx', 11, {
name: '北京市',
code: '100000'
});
console.log(person instanceof Person); // true
使用构造函数创建的对象可以确定其所属类型,解决了方法3存在的问题。但是使用构造函数创建的对象存在一个问题,即相同实例的函数是不一样的。
var person = new Person('kingx', 11, {
name: '北京市',
code: '100000'
});
var person2 = new Person('kingx', 11, {
name: '北京市',
code: '100000'
});
console.log(person.getName === person2.getName); // false
也就是说每一个实例的函数都会占一定的内存空间,这是没必要的。
基于原型对象的模式是将所有的函数和属性都封装在对象的prototype属性上。
// 定义函数
function Person() {}
// 通过prototype属性增加属性和函数
Person.prototype.name = 'kingx';
Person.prototype.age = 11;
Person.prototype.address = {
name: '北京市',
code: '100000'
};
Person.prototype.getName = function () {
return this.name;
};
// 生成两个实例
var person = new Person();
var person2 = new Person();
console.log(person.name === person2.name); // true
console.log(person.getName === person2.getName); // true
不同的实例会共享原型上的属性和参数,也就解决了(4)的问题。
但是也因此,如果改变其中一个实例的属性或值,其他的实例对应也都会发生变化。所以基于原型对象的模式很少单独使用
构造函数和原型混合的模式是目前最常见的创建自定义类型对象的方式。
构造函数中用于定义实例的属性,原型对象中用于定义实例共享的属性和函数。
通过构造函数传递参数,每个实例都能拥有自己的属性值,同时实例还能共享函数的引 用,最大限度地节省了内存空间。结合了两种方式的长处。
// 构造函数中定义实例的属性
function Person(name, age, address) {
this.name = name;
this.age = age;
this.address = address;
}
// 原型中添加实例共享的函数
Person.prototype.getName = function() {
return this.name;
};
// 生成两个实例
var person = new Person('kingx', 11, {
name:'beijing',
code:'100000'
});
var person2 = new Person('kingx2', 12, {
name: '上海市',
code: '200000'
});
// 输出实例初始的name属性值
console.log(person.name); // kingx
console.log(person2.name); // kingx2
// 改变一个实例的属性值
person.address.name = '广州市';
person.address.code = '510000';
// 不影响另一个实例的属性值
console.log(person2.address.name); // 上海市
// 不同的实例共享相同的函数,因此在比较时是相等的
console.log(person.getName === person2.getName); // true
// 改变一个实例的属性,函数仍然能正常执行
person2.name = 'kingx3';
console.log(person.getName()); // kingx
console.log(person2.getName()); // kingx3
动态原型模式是将原型对象放在构造函数内部,通过变量进行控制,只在第一次生成实例的时候进行原型的设置。
动态原型的模式相当于懒汉模式,只在生成实例时设置原型对象,但是功能与构造函数和原型混合模式是相同的。
// 动态原型模式
function Person(name, age, address) {
this.name = name;
this.age = age;
this.address = address;
// 如果Person对象中_initialized 为undefined,则表明还没有为Person的原型对象添加函数
if (typeof Person._initialized === "undefined") {
Person.prototype.getName = function () {
return this.name;
};
Person._initialized = true;
}
}
根据复制后的变量与原始变量值的影响情况,拷贝可以分为浅拷贝和深拷贝两种方式。
针对不同的数据类型,浅拷贝和深拷贝会有不同的表现,主要表现于基本数据类型和引用数据类型在内存中存储的值不同。
对于基本数据类型的值,变量存储的是值本身,存放在栈内存的简单数据段中,可以直接进行访问。
对于引用类型的值,变量存储的是值在内存中的地址,地址指向内存中的某个位置。 如果有多个变量同时指向同一个内存地址,则其中一个变量对值进行修改时,会影响到其他的变量。
基本数据类型不管是浅拷贝还是深拷贝都是对值本身的拷贝,对拷贝后值的修改不会影响到原始值。
引用数据类型如果执行的是浅拷贝,对拷贝后值的修改会影响到原始值;如果执行的是深拷贝,则拷贝的对象和原始对象相互独立,不会彼此影响。
浅拷贝只拷贝对象最外层的属性。如果对象存在更深层的属性,则不会进行处理,这就会导致拷贝对象和原始对象的深层属性仍然指向同一块内存。
有 2 种实现浅拷贝的方法:
也就是遍历对象最外层的所有属性,直接将属性值复制到另一个变量中。
/**
* JavaScript实现对象浅克隆——引用复制
*/
function shallowClone(origin) {
var result = {};
// 遍历最外层属性
for (var key in origin) {
// 判断是否是对象自身的属性
if (origin.hasOwnProperty(key)) {
result[key] = origin[key];
}
}
return result;
}
ES6 中,Object 对象新增了一个assign()
函数,用于将源对象的可枚举属性复制到目标对象中。
var origin = {
a: 1,
b: [2, 3, 4],
c: {
d: 'name'
}
};
// 通过Object.assign()函数克隆对象
var result = Object.assign({}, origin);
console.log(origin); // { a: 1, b: [ 2, 3, 4 ], c: { d: 'name' } }
console.log(result); // { a: 1, b: [ 2, 3, 4 ], c: { d: 'name' } }
目前有多个类库提供了标准的深拷贝实现方法。
如果一个对象中的全部属性都是可以序列化的,那么我们可以先使用JSON.stringify()函数将原始对象序列化为字符串,再使用JSON.parse()函数将字符串反序列化为一个对象,这样得到的对象就是深克隆后的对象。
var origin = {
a: 1,
b: [2, 3, 4],
c: {
d: 'name'
}
};
// 先反序列化为字符串,再序列化为对象,得到深克隆后的对象
var result = JSON.parse(JSON.stringify(origin));
console.log(origin); // { a: 1, b: [ 2, 3, 4 ], c: { d: 'name' } }
console.log(result); // { a: 1, b: [ 2, 3, 4 ], c: { d: 'name' } }
这种方法能够解决大部分JSON类型对象的深克隆问题,但是对于以下几个问题不能很好地解决:
为了解决以上几个问题,我们需要自定义实现深拷贝,对不同的数据类型进行特殊处理,也就是下一种方法
这个是实现深拷贝最好的方式,刚才提到的问题全部可以解决。
在自定义实现深拷贝时,需要针对不同的数据类型做针对性的处理,因此我们会先实现判断数据类型的函数,并将所有函数封装在一个辅助类对象中,这里用“_”表示(类似于underscore类库对外暴露的对象)
/**
* 类型判断
*/
(function (_) {
// 列举出可能存在的数据类型
var types = 'Array Object String Date RegExp Function Boolean Number Null Undefined'.split(' ');
function type() {
// 通过调用toString()函数,从索引为8时截取字符串,得到数据类型的值
return Object.prototype.toString.call(this).slice(8, -1);
}
for (var i = types.length; i--;) {
_['is' + types[i]] = (function (self) {
return function (elem) {
return type.call(elem) === self;
};
})(types[i]);
}
return _;
})(_ = {});
执行上面的代码后,_
对象便具有了isArray()
函数、isObject()
函数等一系列判断数据类型的函数。然后再调用_.isArray(param)
函数判断param
是否是数组类型、调用 _.isObject(param)
函数判断param
是否是对象类型。
然后就是对深拷贝的代码实现,具体看注释。
(这块还没有太理解,之后还需要再看看)
/**
* 深拷贝实现方案
* @param source 待拷贝的对象
* @returns {*} 返回拷贝后的对象
*/
function deepClone(source) {
// 维护两个储存循环引用的数组
var parents = [];
var children = [];
// 用于获得正则表达式的修饰符,/igm
function getRegExp(reg) {
var result = '';
if (reg.ignoreCase) {
result += 'i';
}
if (reg.global) {
result += 'g';
}
if (reg.multiline) {
result += 'm';
}
return result;
}
// 便于递归的_clone()函数
function _clone(parent) {
if (parent === null)
return null;
if (typeof parent !== 'object')
return parent;
var child, proto;
// 对数组做特殊处理
if (_.isArray(parent)) {
child = [];
} else if (_.isRegExp(parent)) {
// 对正则对象做特殊处理
child = new RegExp(parent.source, getRegExp(parent));
if (parent.lastIndex)
child.lastIndex = parent.lastIndex;
} else if (_.isDate(parent)) {
// 对Date对象做特殊处理
child = new Date(parent.getTime());
} else {
// 处理对象原型
proto = Object.getPrototypeOf(parent);
// 利用Object.create切断原型链
child = Object.create(proto);
}
// 处理循环引用
var index = parents.indexOf(parent);
if (index !== -1) {
// 如果父数组存在本对象,说明之前已经被引用过,直接返回此对象
return children[index];
}
// 没有引用过,则添加至parents和children数组中
parents.push(parent);
children.push(child);
// 遍历对象属性
for (var prop in parent) {
if (parent.hasOwnProperty(prop)) {
// 递归处理
child[prop] = _clone(parent[prop]);
}
}
return child;
}
return _clone(source);
}
在jQuery中提供了一个$.clone()
函数,但是它是用于复制DOM对象的。真正用于实现拷贝的函数是$.extend()
。
使用$.extend()
函数可以实现函数与正则表达式等类型的克隆,还能保持拷贝对象的原型链关系,解决了深拷贝中存在的3个问题中的前两个,但是却无法解决循环引用的问题。
由于现在更多使用Vue或React等,因此具体方法就不记录在这里了,只是放在这提醒一下。如果后续有需要再单独学习。
通过构造函数创建实例会导致函数在不同实例中重复创建。
可以通过prototype
属性解决这个问题。每一个函数在创建时都会被赋予一个prototype
属性,它指向函数的原型对象,这个对象可以包含所有实例共享的属性和函数。因此在使用prototype
属性后,就可以将实例共享的属性和函数抽离出构造函数,将它们添加在prototype
属性中。
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function () {
console.log(this.name);
}; // 不同实例中的sayName属性是相等的
每个函数在创建时都会被赋予一个prototype
属性,默认情况下,所有的原型对象都会增加一个constructor
属性,指向prototype
属性所在的函数,即构造函数。
当通过new
操作符调用构造函数创建一个实例时,实例具有一个__proto__
属性,指向构造函数的原型对象,因此__proto__
属性相当于连接实例与构造函数的原型对象的桥梁。
下面的例子为构造函数的原型对象添加了 4 个属性,同时生成了 2 个实例:
function Person(){}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 29;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
以这个 Person 构造函数为例,来看一下构造函数、原型对象和实例之间的关系
构造函数Person有个prototype
属性,指向的是Person的原型对象。在原型对象中有constructor
属性和另外4个原型对象上的属性,其中constructor
属性指向构造函数本身。
通过new操作符创建的两个实例person1和person2,都具有一个__proto__
属性 (图中的[[Prototype]]
即__proto__
属性),指向的是Person的原型对象。
当我们通过对象的实例读取某个属性时,是有一个搜索过程的:
在之前的代码中,每次为原型对象添加一个属性或者函数时,都需要手动写上Person.prototype,这是一种冗余的写法。我们可以将所有需要绑定在原型对象上的属性写成一个对象字面量的形式,并赋值给prototype
属性,需要注意的是要增加一个constructor
属性指向该构造函数:
function Person() {}
Person.prototype = {
constructor: Person, // 重要
name: 'Nicholas',
age: 29,
job: 'Software Engineer',
sayName: function () {
console.log(this.name);
}
};
将一个对象字面量赋给prototype
属性的方式实际是重写了原型对象,等同于切断了构造函数和最初原型之间的关系。因此有一点需要注意的是,如果仍然想使用constructor
属性做后续处理,则应该在对象字面量中增加一个constructor
属性,指向构造函数本身,否则原型的constructor
属性会指向Object类型的构造函数,从而导致constructor
属性与构造函数的脱离。
function Person() {}
Person.prototype = {
name: 'Nicholas',
sayName: function () {
console.log(this.name);
}
};
Person.prototype.constructor === Object; // true
Person.prototype.constructor === Person; // false
由于重写原型对象会切断构造函数和最初原型之间的关系,因此会带来一个隐患,那就是如果在重写原型对象之前,已经生成了对象的实例,则该实例将无法访问到新的原型对象中的函数。只有在重写后生成的实例才能访问到。
因此如果想要重写原型对象,需要保证不要在重写完成之前生成对象的实例,否则会出现异常。
对象的每个实例都具有一个__proto__
属性,指向的是构造函数的原型对象,而原型对象同样存在一个__proto__
属性指向上一级构造函数的原型对象,就这样层层往上,直到最上层某个原型对象为null
。
在JavaScript中几乎所有的对象都具有__proto__
属性,由__proto__
属性连接而成的 链路构成了JavaScript的原型链,原型链的顶端是Object.prototype
,它的__proto__
属 性为null
。
这个图可以分3部分理解:
第一部分是自定义的Foo()函数,Foo()函数的prototype属性指向Foo.prototype对象,通过Foo()构造函数生成实例f1、f2,它们的__proto__
属性指向Foo.prototype对象。而Foo()函数本身可以作为Function对象的实例,因此Foo.__proto__指向Function.prototype。
第二部分与Object()构造函数有关,Object()构造函数本身也是Function类型,因此Object.__proto__指向Function.prototype,通过Object()构造函数生成的实例o1、o2,它们的__proto__
属性指向Object.prototype对象。
第三部分与Function()构造函数有关,Function.__proto__指向Function.prototype,而Function.prototype为一个对象,它的__proto__
属性指向Object.prototype对象。
这样所有的对象通过__proto__
属性向上寻找都是一定会追溯到Object.prototype
的。
关于原型链有一些需要注意的知识点:
可以使用hasOwnProperty()
函数区分属性是实例自身的还是从原型链中继承的,如果是true
说明是实例属性,false
代表是原型对象上的属性。
在使用for...in
运算符遍历对象的属性时,一般可以配合hasOwnProperty()
函数一起使用,检测是否是对象自身的属性,然后做后续处理。
for (var prop in person) {
if (person.hasOwnProperty(prop)) {
// do something
}
}
JavaScript中有一些特定的内置构造函数,如String()构造函数、Number()构造函 数、Array()构造函数、Object()构造函数等。
它们本身的__proto__
属性都统一指向Function.prototype
String._ _proto_ _ === Function.prototype; // true
Number._ _proto_ _ === Function.prototype; // true
Array._ _proto_ _ === Function.prototype; // true
Date._ _proto_ _ === Function.prototype; // true
Object._ _proto_ _ === Function.prototype; // true
Function._ _proto_ _ === Function.prototype; // true
以下面举例:
Function.prototype.a = 'a'; Object.prototype.b = 'b';
function Person() {}
var p = new Person();
console.log('p.a:', p.a); console.log('p.b:', p.b);
上面的代码需要输出实例p的a属性和b属性的值,所以我们需要先了解实例p的属性查找过程。属性的查找是根据__proto__
属性沿着原型链来完成的,因此我们需要先梳理出实例p的原型链:
// 实例p直接原型
p._ _proto_ _ = Person.prototype;
// Person原型对象的原型
Person.prototype._ _proto_ _ = Object.prototype;
因此实例输出p的属性时,最终会找到Object.prototype中去,所以结果是
p.a; // undefined
p.b; // b
虽然JavaScript并不是一门面向对象的语言,不直接具备继承的特性,但是我们可以通过某些方式间接实现继承,从而能利用继承的优势,增强代码复用性与扩展性。在不影响父类对象实现的情况下,使得子类对象具有父类对象的特性;同时还能在不影响父类对象行为的情况下扩展子类对象独有的特性。
本节会介绍 5 种 JavaScript 实现继承的方式。
首先先提前定义一个父类 Animal 并增加属性、实例函数和原型函数:
// 定义一个父类
Animal function Animal(name) {
// 属性
this.type = 'Animal';
this.name = name || '动物';
// 实例函数
this.sleep = function () {
console.log(this.name + '正在睡觉!');
}
}
// 原型函数
Animal.prototype.eat = function (food) {
console.log(this.name + '正在吃:' + food);
};
原型链继承的主要思想:重写子类的prototype
属性,将其指向父类的实例。
// 父类
function Animal(age) {
// 属性
this.name = 'Animal';
this.age = age;
// 实例函数
this.sleep = function () {
return this.name + '正在睡觉!'; }
}
// 父类原型函数
Animal.prototype.eat = function (food) {
return this.name + '正在吃:' + food;
};
// 子类Cat
function Cat(name) {
this.name = name;
}
// 原型继承
Cat.prototype = new Animal();
// 很关键的一句,将Cat的构造函数指向自身
Cat.prototype.constructor = Cat;
var cat = new Cat('加菲猫');
console.log(cat.type); // Animal
console.log(cat.name); // 加菲猫
console.log(cat.sleep()); // 加菲猫正在睡觉!
console.log(cat.eat('猫粮')); // 加菲猫正在吃:猫粮
prototype
属性指向了Animal类型的实例,因此在生成实例cat时,会继承实例函数和原型函数,在调用sleep()函数和eat()函数时,this指向了实例cat,从而输出“加菲猫正在睡觉!”和“加菲猫正在吃:猫粮”需要注意这一句:
Cat.prototype.constructor = Cat;
这是因为如果不将Cat原型对象的constructor属性指向自身的构造函数的话,那将会指向父类Animal的构造函数。
所以在设置了子类的prototype属性后,需要将其constructor属性指向自身构造函数。
构造继承的主要思想:在子类的构造函数中通过call()
函数改变this
的指向,调用父类的构造函数,从而能将父类的实例的属性和函数绑定到子类的this
上。
// 父类
function Animal(age) {
// 属性
this.name = 'Animal';
this.age = age;
// 实例函数
this.sleep = function () {
return this.name + '正在睡觉!'; }
}
// 父类原型函数
Animal.prototype.eat = function (food) {
return this.name + '正在吃:' + food;
};
// 子类
function Cat(name) {
// 核心,通过call()函数实现Animal的实例的属性和函数的继承
Animal.call(this);
this.name = name || 'tom';
}
// 生成子类的实例
var cat = new Cat('tony');
// 可以正常调用父类实例函数
console.log(cat.sleep()); // tony正在睡觉!
// 不能调用父类原型函数
console.log(cat.eat()); // TypeError: cat.eat is not a function
子类可以正常调用父类的实例函数,而无法调用父类原型对象上的函数。
call()
函数实际是改变了父类Animal构造函数中this
的指向,调用后this
指向了子类 Cat,相当于将父类的type、age和sleep等属性和函数直接绑定到了子类的this
中,成了子类实例的属性和函数,因此生成的子类实例中是各自拥有自己的type、age和sleep属性和函数,不会相互影响function Cat(name, parentAge) {
// 在子类生成实例时,传递参数给call()函数,间接地传递给父类,然后被子类继承
Animal.call(this, parentAge);
this.name = name || 'tom';
}
// 生成子类实例
var cat = new Cat('tony', 11);
console.log(cat.age); // 11,因为子类继承了父类的age属性
call()
函数来继承多个父对象,每调用一次call()
函数就会将父类的实例的属性和函数绑定到子类的this中复制继承的主要思想:首先生成父类的实例,然后通过for...in
遍历父类实例的属性和函数,并将其依次设置为子类实例的属性和函数或者原型对象上的属性和函数。
// 父类
function Animal(parentAge) {
// 实例属性
this.name = 'Animal';
this.age = parentAge;
// 实例函数
this.sleep = function () {
return this.name + '正在睡觉!';
}
}
// 原型函数
Animal.prototype.eat = function (food) {
return this.name + '正在吃:' + food; };
// 子类
function Cat(name, age) {
var animal = new Animal(age);
// 父类的属性和函数,全部添加至子类中
for (var key in animal) {
// 实例属性和函数
if (animal.hasOwnProperty(key)) {
this[key] = animal[key];
} else {
// 原型对象上的属性和函数
Cat.prototype[key] = animal[key];
}
}
// 子类自身的属性
this.name = name;
}
// 子类自身原型函数
Cat.prototype.eat = function (food) {
return this.name + '正在吃:' + food;
};
var cat = new Cat('tony', 12); console.log(cat.age); // 12
console.log(cat.sleep()); // tony正在睡觉!
console.log(cat.eat('猫粮')); // tony正在吃:猫粮
在子类的构造函数中,对父类实例的所有属性进行for...in
遍历,如果animal.hasOwnProperty(key)返回 true
,则表示是实例的属性和函数,则直接绑定到子类的this
上,成为子类实例的属性和函数;如果animal.hasOwnProperty(key)返回false
,则表示是原型对象上的属性和函数,则将其添加至子类的prototype属性上,成为子类的原型对象上的属性和函数
for...in
处理即组合继承的主要思想:组合了构造继承和原型继承两种方法,一方面在子类的构造函数中通过call()
函数调用父类的构造函数,将父类的实例的属性和函数绑定到子类的this
中;另一方面,通过改变子类的prototype
属性,继承父类的原型对象上的属性和函数。
// 父类
function Animal(parentAge) {
// 实例属性
this.name = 'Animal';
this.age = parentAge;
// 实例函数
this.sleep = function () {
return this.name + '正在睡觉!';
};
this.feature = ['fat', 'thin', 'tall'];
}
// 原型函数
Animal.prototype.eat = function (food) {
return this.name + '正在吃:' + food;
};
// 子类
function Cat(name) {
// 通过构造函数继承实例的属性和函数
Animal.call(this);
this.name = name;
}
// 通过原型继承原型对象上的属性和函数
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
var cat = new Cat('tony');
console.log(cat.name); // tony
console.log(cat.sleep()); // tony正在睡觉!
console.log(cat.eat('猫粮')); // tony正在吃:猫粮
组合继承的缺点为:父类的实例属性会绑定两次。
在子类的构造函数中,通过call()
函数调用了一次父类的构造函数;在改写子类的 prototype
属性、生成父类的实例时调用了一次父类的构造函数。
通过两次调用,父类实例的属性和函数会进行两次绑定,一次会绑定到子类的构造函数的this中,即实例属性和函数,另一次会绑定到子类的prototype属性中,即原型对象上的属性和函数,但是实例属性优先级会比原型对象上的属性优先级高,因此实例属性会覆盖原型对象上的属性。
其实组合继承方案已经足够好,但是仍然可以对于存在的缺点进行优化:在进行子类的prototype
属性的设置时,可以去掉父类实例的属性和函数。
// 子类
function Cat(name) {
// 继承父类的实例属性和函数
Animal.call(this);
this.name = name;
}
// 立即执行函数
(function () {
// 设置任意函数Super()
var Super = function () {};
// 关键语句,Super()函数的原型指向父类Animal的原型,去掉父类的实例属性
Super.prototype = Animal.prototype;
Cat.prototype = new Super();
Cat.prototype.constructor = Cat;
})();
最关键的一句在于
Super.prototype = Animal.prototype;
只取父类Animal的prototype属性,过滤掉Animal的实例属性,从而避免了父类的实例属性绑定两次。
一般使用组合继承就已经足够,但是寄生组合继承是实现继承最完美的一种。
在之前判断一个变量的类型时,使用了typeof
运算符。但是存在一个问题,对于任何引用数据类型的值都会返回“object”,从而无法判断对象的具体类型。
因此引入了instanceof
运算符:
target instanceof constructor
上面的代码表示的是构造函数constructor()的prototype属性是否出现在target对象的原型链中,说得通俗一点就是,target对象是不是构造函数constructor()的实例。
例子1:原生数据类型的包装类型
var stringObject = new String('hello world');
stringObject instanceof String; // true
例子2:Function类型
function Foo() {}
var foo = new Foo();
foo instanceof Foo; //true
instanceof
很重要的一点就是可以在继承关系中,判断一个实例对象是否属于它的父类。
// 定义构造函数
function C(){}
function D(){}
var o = new C();
o instanceof C; //true
o instanceof D; //false
o instanceof Object; //true,因为Object.prototype属性在o的原型链上
D.prototype = new C(); // 通过将D()构造函数的prototype属性指向C()构造函数的一个实例可以产生继承关系
var o2 = new D();
o2 instanceof D; // true
o2 instanceof C; // true,因为通过继承关系,C.prototype出现在o2的原型链上
从上面看起来instanceof
运算符比较简单,但其实会有很多复杂情况:
Object instanceof Object; //true
Function instanceof Function; //true
Number instanceof Number; //false
String instanceof String; //false
Function instanceof Object; //true
Foo instanceof Function; //true
Foo instanceof Foo; //false
会发现 Object() 构造函数和 Function() 构造函数在使用instanceof
运算符处理自身的时候会返回true
;而 Number() 构造函数和 String() 构造函数在使用instanceof
运算符处理自身的时候返回false
。
以下是一段对instanceof运算符实现原理比较经典的JavaScript代码解释:
/**
* instanceof 运算符实现原理 * @param L 表示左表达式
* @param R 表示右表达式
* @returns {boolean}
*/
function instance_of(L, R) {
var O = R.prototype; // 取 R 的显示原型
L = L._ _proto_ _; // 取 L 的隐式原型
while (true) {
if (L === null)
return false;
if (O === L) // 这里是重点:当 O 严格等于 L 时,返回“true”
return true;
L = L._ _proto_ _; // 如果不相等则重新取L的隐式原型
}
}
对这段代码的理解如下:
// 将左、右侧值进行赋值
ObjectL = Object, ObjectR = Object;
// 根据原理获取对应值
L = ObjectL._ _proto_ _ = Function.prototype;
R = ObjectR.prototype;
// 执行第一次判断
L != R;
// 继续寻找L._ _pro_ _
L = L._ _proto_ _ = Function.prototype._ _proto_ _ = Object.prototype; // 执行第二次判断
L === R; // true
// 将左、右侧值进行赋值
FunctionL = Function, FunctionR = Function;
// 根据原理获取对应值
L = FunctionL._ _proto_ _ = Function.prototype;
R = FunctionR.prototype = Function.prototype; // 执行第一次判断成功,返回“true”
L === R; // true
// 将左、右侧值进行赋值
FooL = Foo, FooR = Foo;
// 根据原理获取对应值
L = FooL._ _proto_ _ = Function.prototype;
R = FooR.prototype = Foo.prototype;
// 第一次判断失败,返回“false”
L !== R;
// 继续寻找L._ _proto_ _
L = L._ _proto_ _ = Function.prototype._ _proto_ _ = Object.prototype; // 第二次判断失败,返回“false”
L !== R;
// 继续寻找L._ _proto_ _
L = L._ _proto_ _ = Object.prototype._ _proto_ _ = null;
// L为null,返回“false”
L === null;
因此Foo instanceof Foo返回false
// 将左、右侧值进行赋值
StringL = String, StringR = String;
// 根据原理获取对应值
L = StringL._ _proto_ _ = Function.prototype;
R = StringR.prototype = String.prototype;
// 第一次判断失败,返回“false”
L !== R;
// 继续寻找L._ _proto_ _
L = L._ _proto_ _ = Function.prototype._ _proto_ _ = Object.prototype; // 第二次判断失败,返回“false”
L !== R;
// 继续寻找L._ _proto_ _
L = L._ _proto_ _ = Object.prototype._ _proto_ _ = null; // L为null,返回“false”
L === null;
因此String instanceof String返回false