对象的属性
前言
查看一个对象属性的最好方法,不是去百度,也不是去google,而是用下面的方法:Reflect.ownKeys(Object);
1. 数据属性
数据属性具有4个描述其行为的特性,因为这些特性是内部值,所以ECMA-262规范将其放在了两对方括号中。
- [[Configurable]]:表示属性能否删除而重新定义,或者是否可以修改为访问器属性,默认值为true。
- [[Enumerable]]:表示属性是否可枚举,可枚举的属性能够通过for…in循环返回,默认值为true。
- [[Writable]]:表示属性值能否被修改,默认值为true。
[[Value]]:表示属性的真实值,属性的读取和写入均通过此属性完成,默认值为undefined。
2. 访问器属性
访问器属性同样包含4个特性,分别是[[Configurable]]、[[Enumerable]]、[[Get]]和[[Set]]。
[[Configurable]]:表示能否修改为访问器属性,默认值为true。
- [[Enumerable]]:表示属性是否可枚举,可枚举的属性能够通过for…in循环返回,默认值为true。
- [[Get]]:在读取属性值时调用的函数(一般称为getter()函数),负责返回有效的值,默认值为undefined。
- [[Set]]:在写入属性值时调用的函数(一般称为setter()函数),负责处理数据,默认值为undefined。
getter()函数和setter()函数的存在在一定程度上可以实现对象的私有属性,私有属性不对外暴露。如果想要读取和写入私有属性的值,则需要通过设置额外属性的getter()函数和setter()函数来实现
如果需要修改数据属性或访问器属性默认的特性,则必须使用Object.defineProperty()函数。
var person = {
_age: 10, // 私有属性
name: 'xjp'
}
// 修改对象的数据属性
Object.defineProperty(person, name, {
configurable: true,
enumerable: false,
writable: false,
value: 'kingx'
});
// 修改对象的访问器属性
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
console.log(person.age) // 10
person.age = 19 // “设置成功”
console.log(person.age) // 19
3. 属性的访问方式区别
使用“.”与“[ ]”来访问属性区别:
第一点,点操作符是静态的,只能是一个以属性名称命名的简单描述符,而且无法修改;而中括号操作符是动态的,可以传递字符串或者变量,并且支持在运行时修改。
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
4. 属性遍历
到ES6为止,一共有5种方法可以实现对象属性的遍历:
- for…in。// 用于遍历对象自身和继承的可枚举属性(不包含Symbol属性)
- Object.keys(obj)。 // 返回一个数组,包含对象自身所有可枚举属性,不包含继承属性和Symbol属性。
- Object.getOwnPropertyNames(obj)。// 返回一个数组,包含对象自身所有可枚举属性和不可枚举属性,不包含继承属性和Symbol属性。
- Object.getOwnPropertySymbols(obj)。// 返回一个数组,包含对象自身所有Symbol属性,不包含其他属性。
- Reflect.ownKeys(obj)。// 返回一个数组,包含可枚举属性、不可枚举属性以及Symbol属性,不包含继承属性。
创建对象
1. 基于工厂方法模式
工厂方法模式是一种比较重要的设计模式,用于创建对象,旨在抽象出创建对象和属性赋值的过程,只对外暴露出需要设置的属性值。
优点: 使用工厂方法可以减少很多重复的代码。// 工厂方法,对外暴露接收的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类型,无法更进一步区分具体的类型。2. 基于构造函数模式
构造函数是通过this为对象添加属性的,属性值类型可以为基本类型、对象或者函数,然后通过new操作符创建对象的实例。 ```javascript 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
var person1 = new Person(‘kingx’, 11, { name: ‘北京市’, code: ‘100000’ }) var person2 = new Person(‘kingx’, 11, { name: ‘北京市’, code: ‘100000’ })
console.log(person1.address === person2.address) // false console.log(person1.getName === person2.getName) // false
**优点: 使用构造函数创建的对象可以确定其所属类型,解决了工厂模式存在的问题。**<br />**缺点: 相同实例的函数与对象是不一样的。这就意味着每个实例的函数和对象都会占据一定的内存空间,会造成资源的浪费,另外函数也没有必要在代码执行前就绑定在对象上。**
<a name="S9nQ5"></a>
## 3. 基于原型对象的模式
基于原型对象的模式是将所有的函数和属性都封装在对象的prototype属性上。
```javascript
// 定义函数
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.address === person2.address) // true
console.log(person.getName === person2.getName) // true
console.log(person.address) // { name: '北京市', code: '100000' }
person2.address.name = '江苏'
console.log(person.address) // { name: '江苏', code: '100000' }
优点: 使用基于原型对象的模式创建的实例,其所有属性和函数都是相等的,不同的实例会共享原型上的属性和函数,解决了构造函数模式存在的问题。
缺点: 所有的实例会共享相同的属性,那么改变其中一个实例的属性值,会引起其他实例的属性值变化,这并不是我们所期望的,所以基于原型对象的模式很少会单独使用。
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: '北京市',
code: '100000'
})
var person2 = new Person('kingx2', 12, {
name: '上海市',
code: '200000'
})
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
5. 基于动态原型模式
动态原型模式是将原型对象放在构造函数内部,通过变量进行控制,只在第一次生成实例的时候进行原型的设置。动态原型的模式相当于懒汉模式,只在生成实例时设置原型对象,但是功能与构造函数和原型混合模式是相同的。
// 动态原型模式
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;
}
}
// 生成两个实例
var person = new Person('kingx', 11, {
name: '北京市',
code: '100000'
});
var person2 = new Person('kingx2', 12, {
name: '上海市',
code: '200000'
});
// 改变其中一个实例的属性
person.address.name = '广州市';
person.address.code = '510000';
// 不会影响到另一个实例的属性
console.log(person2.address.name);
// 上海市
// 改变一个实例的属性,函数仍然能正常执行
person2.name = 'kingx3';
console.log(person.getName()); // kingx
console.log(person2.getName()); // kingx3
对象克隆
1. 对象浅克隆
浅克隆由于只克隆对象最外层的属性,如果对象存在更深层的属性,则不进行处理,导致克隆对象和原始对象的深层属性仍然指向同一块内存,修改克隆对象的深层属性的值会影响到原始对象的深层属性值。
1. 简单的引用复制(for in 或 Object.keys)
遍历对象最外层的所有属性,直接将属性值复制到另一个变量中:
/**
* JavaScript实现对象浅克隆——引用复制
*/
function shallowClone(origin) {
var result = {}
// 遍历最外层属性
for (var key in origin) { //for in会输出自身以及原型链上可枚举的属性。Object.keys用来获取对象自身可枚举的属性键,Object.getOwnPropertyNames用来用来获取对象自身的全部属性名。Object.keys的效果和for in+hasOwnProperty的效果是一样的。
// 判断是否是对象自身的属性
if (origin.hasOwnProperty(key)) {
result[key] = origin[key]
}
}
return result
}
// 原始对象
var origin = {
a: 1,
b: [2, 3, 4],
c: {
d: 'name'
}
}
// 克隆后的对象
var result = shallowClone(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' } }
result.c.d = 'abc'
console.log(origin) // { a: 1, b: [ 2, 3, 4 ], c: { d: 'abc' } }
console.log(result) // { a: 1, b: [ 2, 3, 4 ], c: { d: 'abc' } }
2. 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' } }
2. 对象深克隆
深拷贝就是浅拷贝的递归,同时加上对一些数据类型(Date、正则、数组)以及循环引用的处理判断。但是深拷贝地址引用变了。
1. JSON序列化和反序列化
如果一个对象中的全部属性都是可以序列化的,那么我们可以先使用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深克隆问题:
- 无法实现对函数、RegExp等特殊对象的克隆。
- 对象的constructor会被抛弃,所有的构造函数会指向Object,原型链关系断裂。
- 对象中如果存在循环引用,会抛出异常(Uncaught TypeError: Converting circular structure to JSON),而不是因为死循环而导致栈溢出,JSON.stringify内部做了循环引用的检测。 ```javascript function Animal(name) { this.name = name } let animal = new Animal(‘tom’)
// 原始对象 let origin = { // 属性为函数 a: function () { return ‘a’ }, // 属性为正则表达式对象 b: new RegExp(‘\d’, ‘g’), // 属性为某个对象的实例 c: animal } let result = JSON.parse(JSON.stringify(origin)) let result = JSON.parse(JSON.stringify(origin))
console.log(origin) // { a: [Function: a], b: /d/g, c: Animal { name: ‘tom’ } } console.log(origin.c.constructor) // [Function: Animal] console.log(result) // { b: {}, c: { name: ‘tom’ } } console.log(result.c.constructor) // [Function: Object]
origin.d = origin // 循环引用 let result = JSON.parse(JSON.stringify(origin)) // TypeError: Converting circular structure to JSON
得出结论:
- 值为Function类型的a属性丢失。
- b属性应该为一个正则表达式,在克隆后得到的是一个空对象。
- c属性值虽然都是一个具有name属性的对象,但是克隆后对象的c属性值对象的构造函数却不再指向Animal,而是指向Object,构造函数被丢失,导致原型链关系的断裂。
- 当对循环引用的对象调用JSON.stringify()时,就会抛出异常,表示“循环引用的结构无法序列化成JSON字符串”
<a name="I8RaN"></a>
### 2. 自定义实现深克隆
在自定义实现深克隆时,需要针对不同的数据类型做针对性的处理,因此我们会先实现判断数据类型的函数,并将所有函数封装在一个辅助类对象中,这里用“_”表示(类似于underscore类库对外暴露的对象)。
```javascript
/**
* 深克隆实现方案
* @param obj 待克隆的对象
* @returns {*} 返回克隆后的对象
*/
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj // null 和 undefined
if (typeof obj !== 'object') return obj // 基本类型
if (obj instanceof RegExp) return new RegExp(obj) // 对正则对象做特殊处理
if (obj instanceof Date) return new Date(obj.getTime()) // 对Date对象做特殊处理
if (hash.has(obj)) return hash.get(obj) // 递归的时候处理循环引用,返回上次的结果,不再重复递归
// 数组或者对象
const copy = new obj.constructor
hash.set(obj, copy)
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
// 递归,一直将对象的所有属性全部层级拷贝出来
copy[key] = deepClone(obj[key], hash)
}
}
return copy
}
3. jQuery实现
在jQuery中提供了一个$.clone()函数,但是它是用于复制DOM对象的。真正用于实现克隆的函数是**$.extend()**
,源码如下:
jQuery.extend = jQuery.fn.extend = function () {
// options是一个缓存变量,用来缓存arguments[i],
// name是用来接收将要被扩展对象的key,src改变之前target对象上每个key对应的value
// copy传入对象上每个key对应的value,copyIsArray判定copy是否为一个数组
// clone深克隆中用来临时存对象或数组的src
var src, copyIsArray, copy, name, options, clone,
target = arguments[0] || {},
i = 1,
length = arguments.length,
deep = false
// 如果传递的第一个参数为boolean类型,为true代表深克隆,为false代表浅克隆
if (typeof target === "boolean") {
deep = target
// 如果传递了第一个参数为boolean值,则待克隆的对象为第二个参数
target = arguments[i] || {}
i++
}
// 如果是简单类型数据
if (typeof target !== "object" && !jQuery.isFunction(target)) {
target = {}
}
// 如果只传递一个参数,那么克隆的是jQuery自身
if (i === length) {
target = this
i--
}
for (; i < length; i++) {
// 仅需要处理不是null与undefined类型的数据
if ((options = arguments[i]) != null) {
// 遍历对象的所有属性
for (name in options) {
src = target[name]
copy = options[name]
// 阻止循环引用
if (target === copy) {
continue
}
// 递归处理对象和数组
if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
if (copyIsArray) {
copyIsArray = false
clone = src && jQuery.isArray(src) ? src : []
} else {
clone = src && jQuery.isPlainObject(src) ? src : {}
}
// 将原始值的name属性值赋给target目标对象
target[name] = jQuery.extend(deep, clone, copy)
} else if (copy !== undefined) {
// 对于简单类型,直接赋值
target[name] = copy
}
}
}
}
// 返回clone后的目标对象
return target
};
$.extend()函数无法解决循环引用的问题:
var origin = {};
origin.d = origin;
// Uncaught RangeError: Maximum call stack size exceeded
var result = $.extend(true, {}, origin);
原型与原型链
原型对象
1. 原型对象、构造函数和实例之间的关系
每一个函数在创建时都会被赋予一个prototype属性。在默认情况下,所有的原型对象都会增加一个constructor属性,指向prototype属性所在的函数,即构造函数。构造函数的prototype属性会指向它的原型对象,而通过构造函数可以生成具体的实例。
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有个prototype属性,指向的是Person的原型对象。在原型对象中有constructor属性和另外4个原型对象上的属性,其中constructor属性指向构造函数本身。通过new操作符创建的两个实例person1和person2,都具有一个proto属性(图中的[[Prototype]]),指向的是Person的原型对象。
2. 重写原型对象
每次为原型对象添加一个属性或者函数时,都需要手动写上Person.prototype,这是一种冗余的写法。我们可以将所有需要绑定在原型对象上的属性写成一个对象字面量的形式,并赋值给prototype属性。
function Person() { }
Person.prototype = {
constructor: Person, // 重要
name: 'Nicholas',
age: 29,
job: 'Software Engineer',
sayName: function () {
console.log(this.name)
}
}
Person.prototype = {
name: 'Nicholas',
sayName: function () {
console.log(this.name)
}
}
Person.prototype.constructor === Person // false
Person.prototype.constructor === Object // true
将一个对象字面量赋给prototype属性的方式实际是重写了原型对象,等同于切断了构造函数和最初原型之间的关系。如果仍然想使用constructor属性做后续处理,则应该在对象字面量中增加一个constructor属性,指向构造函数本身,否则原型的constructor属性会指向Object类型的构造函数,从而导致constructor属性与构造函数的脱离,但是其它原型属性都可以继续使用!
弊端:由于重写原型对象会切断构造函数和最初原型之间的关系,因此会带来一个隐患,那就是如果在重写原型对象之前,已经生成了对象的实例,则该实例将无法访问到新的原型对象中的函数。如重写原型对象,不要在重写完成之前生成对象的实例,否则会出现异常。
function Person() { }
// 先生成一个实例person1
var person1 = new Person()
// 重写对象的原型,不添加constructor
Person.prototype = {
name: 'Nicholas',
sayName: function () {
console.log(this.name)
}
}
// 再生成一个实例person2
var person2 = new Person()
person1.sayName() // TypeError: person1.sayName is not a function
person2.sayName() // Nicholas
原型链
对象的每个实例都具有一个proto属性,指向的是构造函数的原型对象,而原型对象同样存在一个proto属性指向上一级构造函数的原型对象,就这样层层往上,直到最上层某个原型对象为null。原型链的顶端是Object.prototype,它的proto属性为null。
重点只需要记住:内置构造函数(**String()构造函数**
、**Number()构造函数**
、**Array()构造函数**
、**Object()构造函数**
、**Function构造函数**
)的_**_proto__**
属性都统一指向**Function.prototype**
,而Function.prototype与其它自定义构造函数的proto均指向Object.prototype,Object.prototype.proto指向null
function Person() { }
var person = new Person()
// 第一次追溯
person.__proto__ === Person.prototype // true
// 第二次追溯
person.__proto__.__proto__ === Person.prototype.__proto__ === Object.prototype
// 第三次追溯
person.__proto__.__proto__.__proto__ = Object.prototype.__proto__ === null
性能:由于属性查找会经历整个原型链,因此查找的链路越长,对性能的影响越大。
内置构造函数:String()构造函数、Number()构造函数、Array()构造函数、Object()构造函数、Function构造函数等。它们本身的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()
// p.__proto__ = Person.prototype; Person.prototype.__proto__ = Object.prototype
console.log(p.a) // undefined
console.log(p.b) // b
⭐对象继承
1. 原型继承
原理:重写子类的prototype属性,将其指向父类的实例,同时将constructor属性指向自身。
关键代码:子类.prototype = new 父类();``子类.prototype.constructor = 子类
// 定义一个父类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)
}
// 子类Cat
function Cat(name) {
this.name = name
}
Cat.prototype = new Animal() // 原型继承
Cat.prototype.constructor = Cat // 很关键的一句,将Cat的构造函数指向自身
var cat = new Cat('加菲猫')
console.log(cat.type) // Animal
console.log(cat.name) // 加菲猫 覆盖父类Animal的name属性值
console.log(cat.sleep()) // 加菲猫正在睡觉!
console.log(cat.eat('猫粮')) // 加菲猫正在吃:猫粮
优点:
(1)简单,易于实现。
(2)继承关系纯粹:生成的实例既是子类的实例,也是父类的实例。
console.log(cat instanceof Cat) // true,是子类的实例
console.log(cat instanceof Animal) // true,是父类的实例
(3)可通过子类直接访问父类原型链原有属性以及新增的属性。
// 父类原型链上增加属性
Animal.prototype.bodyType = 'small'
// 父类原型链上增加函数
Animal.prototype.run = function () {
return this.name + '正在奔跑'
}
// 结果验证
console.log(cat.bodyType) // small
console.log(cat.run()) // 加菲猫正在奔跑
缺点:
(1)子类的所有实例将共享父类的属性:因为子类继承的都是父类new出来的那一个实例。
// 定义父类
function Animal() {
this.feature = ['fat', 'thin', 'tall']
}
// 定义子类
function Cat() { }
// 原型链继承
Cat.prototype = new Animal()
Cat.prototype.constructor = Cat
var cat1 = new Cat()
var cat2 = new Cat()
// 先输出两个实例的feature值
console.log(cat1.feature) // [ 'fat', 'thin', 'tall' ]
console.log(cat2.feature) // [ 'fat', 'thin', 'tall' ]
// 改变cat1实例的feature值
cat1.feature.push('small')
// 再次输出两个实例的feature值,发现cat2实例也受到影响
console.log(cat1.feature) // [ 'fat', 'thin', 'tall', 'small' ]
console.log(cat2.feature) // [ 'fat', 'thin', 'tall', 'small' ]
(2)在创建子类实例时,无法向父类的构造函数传递参数。
(3)无法实现多继承:子类的prototype属性只能设置为一个值,如果同时设置为多个值的话,后面的值会覆盖前面的值,导致子类只能继承一个父类,而无法实现多继承。
(4)为子类增加原型对象上的属性和函数时,必须放在子类.prototype = new 父类()
之后,否则后面重写prototype属性,导致之前设置的全部失效。
2. 构造继承
原理:在子类的构造函数中通过call()函数调用父类的构造函数改变this的指向,从而能将父类的实例的属性和函数绑定到子类的this上。
关键代码:父类.call(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
优点:
(1)可解决子类实例共享父类属性的问题:new出来的每个子类实例都是独立的,不相互影响。
(2)创建子类的实例时,可以向父类传递参数:在call()函数中传递参数,就可以对父类的属性进行设置,同时由子类继承下来。
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属性
(3)可以实现多继承:在子类的构造函数中,可以多次调用call()函数来继承多个父对象,每调用一次call()函数就会将父类的实例的属性和函数绑定到子类的this中。
缺点:
(1)实例只是子类的实例,并不是父类的实例:因为并未通过原型对象将子类与父类进行串联,所以生成的实例与父类并没有关系,并且只能继承父类实例的属性和函数,并不能继承父类原型对象上的属性和函数,这样就失去了继承的意义。
var cat = new Cat('tony')
console.log(cat instanceof Cat) // true,实例是子类的实例
console.log(cat instanceof Animal) // false,实例并不是父类的实例
(2)无法复用父类的实例函数:由于父类的实例函数将通过call()函数绑定到子类的this中,因此子类生成的每个实例都会拥有父类实例函数的引用,这会造成不必要的内存消耗,影响性能。
3. 组合继承
原理:组合了原型继承和构造继承两种方法:一方面在子类的构造函数中通过call()函数调用父类的构造函数,将父类的实例的属性和函数绑定到子类的this中;另一方面,通过改变子类的prototype属性,继承父类的原型对象上的属性和函数。
关键代码:父类.call(this);``子类.prototype = new 父类();``子类.prototype.constructor = 子类
// 父类
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正在吃:猫粮
优点:
(1)既能继承父类实例的属性和函数,又能继承原型对象上的属性和函数。
(2)既是子类的实例,又是父类的实例。
(3)不存在引用属性共享的问题:因为在子类的构造函数中已经将父类的实例属性指向了子类的this。
(4)可以向父类的构造函数中传递参数:通过call()函数可以向父类的构造函数中传递参数。
缺点:唯一缺点为父类的实例属性会继承两次。在子类的构造函数中,通过call()函数调用了一次父类的构造函数;在改写子类的prototype属性、生成父类的实例时调用了一次父类的构造函数。
4. ⭐寄生组合继承
原理:在改写子类的prototype属性的设置时,去掉父类实例的属性和函数,只取父类的原型对象
关键代码:Super.prototype = Animal.prototype;
// 子类
function Cat(name) {
// 继承父类的实例属性和函数
Animal.call(this)
this.name = name
}
// 立即执行函数
(function () {
// 设置任意函数Super()
var Super = function () { }
// 关键语句,Super()函数的原型指向父类Animal的原型
// 只取父类的prototype属性,过滤掉Animal的实例属性,从而避免了父类的实例属性绑定两次。
Super.prototype = Animal.prototype
Cat.prototype = new Super()
Cat.prototype.constructor = Cat
})()
5. 复制继承(了解)
原理:首先生成父类的实例,然后通过for…in遍历父类实例的属性和函数,并将其依次设置为子类实例的属性和函数或者原型对象上的属性和函数。
关键代码:for...in遍历父类实例,通过hasOwnProperty设置为子类实例属性还是子类原型属性
// 父类
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正在吃:猫粮
优点:
(1)支持多继承:只需要在子类的构造函数中生成多个父类的实例,然后通过相同的for…in处理即可。
(2)能同时继承实例的属性与原型对象上的属性
(3)可以向父类构造函数中传参
缺点:
(1)父类的所有属性都需要复制,消耗内存:对于父类的所有属性都需要复制一遍,这会造成内存的重复利用,降低性能。
(2)实例只是子类的实例,并不是父类的实例:只是通过遍历父类的属性将其复制至子类上,并没有通过原型对象串联起父类和子类。
instanceof运算
// 定义构造函数
function C() { }
function D() { }
var o = new C()
o instanceof C // true
o instanceof Object // true,因为Object.prototype属性在o的原型链上
D.prototype = new C() // 继承
var o2 = new D()
o2 instanceof D // true
o2 instanceof C // true,因为通过继承关系,C.prototype出现在o2的原型链上
如果o2 instanceof C
因为继承返回“true”,并不意味着这个表达式会永远返回“true”,我们可以有两种方法改变这个结果。
// 第一个方法,改变构造函数的prototype属性值。只能改变继承的。
D.prototype = {}
var o3 = new D()
o3 instanceof D // true
o3 instanceof C // false
// 第二种方法,改变实例的原型链,使改变后构造函数不在实例的原型链上。原生的与继承的都被改变。
o3.__proto__ = {};
o3 instanceof D; // 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) {
// 实际上只有Object.prototype.__proto__属性为null,即到了原型链的最顶层。
return false
}
if (O === L) {
// 这里是重点:当 O 严格等于 L 时,返回“true”
return true
}
L = L.__proto__ // 如果不相等则递归L的__proto__属性,直到L为null或者O===L,得到最终结果
}
}
Object instanceof Object //true
// 将左、右侧值进行赋值
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
String instanceof String //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
// 同理:
Function instanceof Object //true
Function instanceof Function //true
Number instanceof Number //false