ES 6+ 原型对象
每个 实例对象 都有一个 constructor
属性,指向它的构造函数
function Person(name) {
this.name = name;
}
var person = new Person('Steve'); // 自定义构造函数
var string = new String('Hello'); // 原生的构造函数
console.log(person.constructor === Person); // 执行结果:true
console.log(string.constructor === String); // 执行结果:true
每个 函数对象(包括构造函数) 都有一个 prototype
属性,指向函数的原型对象
这个 原型对象 的 constructor
属性,指向函数本身
function Person(name) {
this.name = name;
}
var person = new Person('Steve'); // 自定义构造函数
var string = new String('Hello'); // 原生的构造函数
console.log(Person.prototype.constructor === Person); // 执行结果:true
console.log(String.prototype.constructor === String); // 执行结果:true
每个 对象 都有一个 [[prototype]]
私有属性,指向它的构造函数的原型对象,但这个属性是不允许访问的
某些浏览器(例如 Chrome)提供 __proto__
属性用于访问 [[prototype]]
私有属性
function Person(name) {
this.name = name;
}
var person = new Person('Steve'); // 自定义构造函数
var string = new String('Hello'); // 原生的构造函数
console.log(person.__proto__ === Person.prototype); // 执行结果:true
console.log(string.__proto__ === String.prototype); // 执行结果:true
console.log(Person.__proto__ === Function.prototype); // 执行结果:true
console.log(String.__proto__ === Function.prototype); // 执行结果:true
构造函数的 constructor
属性都是指向 Function
,__proto__
属性都是指向 Function.prototype
因为构造函数都是通过 new Function
来创建的,它们都是 Function 的实例对象,包括 Function 和 Object
function Person(name) {
this.name = name;
}
console.log(Person.constructor === Function); // 执行结果:true
console.log(Person.__proto__ === Function.prototype); // 执行结果:true
console.log(String.constructor === Function); // 执行结果:true
console.log(String.__proto__ === Function.prototype); // 执行结果:true
console.log(Function.constructor === Function); // 执行结果:true
console.log(Function.__proto__ === Function.prototype); // 执行结果:true
console.log(Object.constructor === Function); // 执行结果:true
console.log(Object.__proto__ === Function.prototype); // 执行结果:true
除 Object
外,其它构造函数的 prototype
属性的 __proto__
属性都是指向 Object.prototype
而 Object
的 prototype
属性的 __proto__
属性指向 null
function Person(name) {
this.name = name;
}
console.log(Person.prototype.__proto__ === Object.prototype); // 执行结果:true
console.log(String.prototype.__proto__ === Object.prototype); // 执行结果:true
console.log(Function.prototype.__proto__ === Object.prototype); // 执行结果:true
console.log(Object.prototype.__proto__ === null); // 执行结果:true
最后来一张图片总结一下,虽然看起来有点复杂,但是只要大家找到规律,就会很简单了
建议大家分类来看,注意哪里出现 constructor
属性,哪里出现 prototype
属性,哪里出现 __proto__
属性
直白的说,原型对象是系统对每种对象进行原始的配置设置,在学习内置对象的时候,有没有发现,每种对象都具备自己独有的一些属性和方法
原型链设计
(1)原型链
上面我们说过,所有对象都有 __proto__
属性,并且这个 __proto__
属性指向一个原型对象
因为原型对象也是对象,这个原型对象也有 __proto__
属性,我们把这种关系称为原型链
(2)属性访问
当需要访问一个对象的属性时,首先从该对象开始查找,如果能够找到,那么到此返回
如果没有找到,就在该对象的 __proto__
属性指向的原型对象中继续查找,如果能够找到,那么到此返回
如果没有找到,那么一直往上查找原型对象,直至 __proto__
属性指向 null
,也就是原型链的顶端
若原型链上的所有原型对象都没有该属性,则返回 undefined
function Person(name) { this.name = name; }
Person.prototype.getName = function() { return this.name; }
var person = new Person('Steve');
var name = person.getName(); // 在 Person.prototype 中找到
var description = person.toString(); // 在 Object.prototype 中找到
var age = person.age; // 在原型链中无法找到
console.log(name); // 执行结果:Steven
console.log(description); // 执行结果:[object Object]
console.log(age); // 执行结果:undefined
(3)属性检测
in
操作符:检测属性是否在对象的原型链上hasOwnProperty
方法:检测属性是否来自对象本身
function Person(name) { this.name = name; }
Person.prototype.getName = function() { return this.name; }
var person = new Person('Steve');
console.log('age' in person); // 执行结果:false
console.log('name' in person); // 执行结果:true
console.log('getName' in person); // 执行结果:true
console.log('toString' in person); // 执行结果:true
console.log(person.hasOwnProperty('age')); // 执行结果:false
console.log(person.hasOwnProperty('name')); // 执行结果:true
console.log(person.hasOwnProperty('getName')); // 执行结果:false
console.log(person.hasOwnProperty('toString')); // 执行结果:false
原型对象的作用
JavaScript 中的继承是基于原型的,原型的作用在于不同实例对象之间可以共享数据,节省内存
function Person(name) { this.name = name; }
Person.prototype.getName = function() { return this.name; }
var person1 = new Person('Steve');
var person2 = new Person('Steven');
// 不同实例对象 person1 和 person2 都可以使用在 Person.prototype 上定义的 getName(共享数据)
// 从而避免在每个实例对象上都要重复定义 getName(节约内存)
var name1 = person1.getName();
var name2 = person2.getName();
console.log(name1);
console.log(name2);
原型继承
方法:将子类的原型指向父类的实例
原理:子类在访问属性或调用方法时,往上查找原型链,能够找到父类的属性和方法
- 缺点:在调用子类构造函数时,无法向父类构造函数传递参数
- 优点:子类实例可以访问父类的实例方法和原型方法
- 缺点:所有子类实例共享父类的引用属性
function SuperType(name, info) {
// 实例属性(基本类型)
this.name = name || 'Super';
// 实例属性(引用类型)
this.info = info || ['Super'];
// 实例方法
this.getName = function() { return this.name; };
}
// 原型方法
SuperType.prototype.getInfo = function() { return this.info; };
// 原型继承
function ChildType(message) { this.message = message; }
ChildType.prototype = new SuperType('Child', ['Child']);
// 在调用子类构造函数时,无法向父类构造函数传递参数
var child = new ChildType('Hello');
// 子类实例可以访问父类的实例方法和原型方法
console.log(child.getName()); // Child
console.log(child.getInfo()); // ["Child"]
// 所有子类实例共享父类的引用属性
var other = new ChildType('Hi');
other.info.push('Temp');
console.log(other.info); // ["Child", "Temp"]
console.log(child.info); // ["Child", "Temp"]
构造继承
方法:在子类的构造函数调用父类的构造函数,并将 this
指向子类实例
原理:在构造子类时,调用父类的构造函数初始化子类的属性和方法
- 优点:在调用子类构造函数时,可以向父类构造函数传递参数
- 缺点:子类实例可以访问父类的实例方法,但是不能访问父类的原型方法,因此无法做到函数复用
- 优点:每个子类实例的属性独立存在
function SuperType(name, info) {
// 实例属性(基本类型)
this.name = name || 'Super';
// 实例属性(引用类型)
this.info = info || ['Super'];
// 实例方法
this.getName = function() { return this.name; };
}
// 原型方法
SuperType.prototype.getInfo = function() { return this.info; };
// 构造继承
function ChildType(name, info, message) {
SuperType.call(this, name, info);
this.message = message;
}
// 在调用子类构造函数时,可以向父类构造函数传递参数
var child = new ChildType('Child', ['Child'], 'Hello');
// 子类实例可以访问父类的实例方法,但是不能访问父类的原型方法
console.log(child.getName()); // Child
console.log(child.getInfo()); // Uncaught TypeError
// 每个子类实例的属性独立存在
var other = new ChildType('Child', ['Child'], 'Hi');
other.info.push('Temp');
console.log(other.info); // ["Child", "Temp"]
console.log(child.info); // ["Child"]
组合继承
方法:同时使用原型继承和构造继承,综合两者的优势所在
原理:通过原型继承实现原型属性和原型方法的继承,通过构造继承实现实例属性和实例方法的继承
- 优点:在调用子类构造函数时,可以向父类构造函数传递参数
- 优点:子类实例可以访问父类的实例方法和原型方法
- 优点:每个子类实例的属性独立存在
- 缺点:在实现组合继承时,需要调用两次父类构造函数
function SuperType(name, info) {
// 实例属性(基本类型)
this.name = name || 'Super';
// 实例属性(引用类型)
this.info = info || ['Super'];
// 实例方法
this.getName = function() { return this.name; };
}
// 原型方法
SuperType.prototype.getInfo = function() { return this.info; };
// 组合继承
function ChildType(name, info, message) {
SuperType.call(this, name, info);
this.message = message;
}
ChildType.prototype = new SuperType();
ChildType.prototype.constructor = ChildType;
// 在调用子类构造函数时,可以向父类构造函数传递参数
var child = new ChildType('Child', ['Child'], 'Hello');
// 子类实例可以访问父类的实例方法和原型方法
console.log(child.getName()); // Child
console.log(child.getInfo()); // ["Child"]
// 每个子类实例的属性独立存在
var other = new ChildType('Child', ['Child'], 'Hi');
other.info.push('Temp');
console.log(other.info); // ["Child", "Temp"]
console.log(child.info); // ["Child"]
原型式继承
方法:实现一个函数,传入已有对象,在函数内部将新对象的原型指向原有对象,最后返回新对象
原理:返回的新对象继承原有对象,然后根据需求对得到的对象加以修改即可
- 要求:创建子类实例必须基于一个已有对象
- 缺点:所有新创建的实例都会重新定义已有对象的实例方法,因此无法做到函数复用
- 缺点:所有新创建的实例共享已有对象的引用属性
var superObject = {
name: 'Super',
info: ['Super'],
getName: function() { return this.name; }
};
// 原型式继承
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
// 创建子类实例必须基于一个已有对象
var childObject = object(superObject);
// 根据需求对得到的对象加以修改
childObject.message = 'Hello';
// 新创建的实例可以访问已有对象的实例属性和实例方法
console.log(childObject.name); // Super
console.log(childObject.getName()); // Super
// 所有新创建的实例共享已有对象的引用属性
var otherObject = object(superObject);
otherObject.info.push('Temp');
console.log(otherObject.info); // ["Child", "Temp"]
console.log(childObject.info); // ["Child", "Temp"]
寄生式继承
方法:创建一个用于封装继承过程的函数,在函数内部以某种方式增强对象,且最后返回对象
原理:基于原型式继承,类似于工厂模式,将增强对象的过程封装到一个函数中
- 要求:创建子类实例必须基于一个已有对象
- 缺点:所有新创建的实例都会重新定义已有对象的实例方法,因此无法做到函数复用
- 缺点:所有新创建的实例共享已有对象的引用属性
var superObject = {
name: 'Super',
info: ['Super'],
getName: function() { return this.name; }
};
// 寄生式继承
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function objectFactory(o) {
var clone = object(o); // 创建对象
clone.message = 'Hello'; // 增强对象
return clone; // 返回对象
}
// 创建子类实例必须基于一个已有对象
var childObject = objectFactory(superObject);
// 新创建的实例可以访问已有对象的实例属性和实例方法
console.log(childObject.name); // Super
console.log(childObject.getName()); // Super
// 所有新创建的实例共享已有对象的引用属性
var otherObject = object(superObject);
otherObject.info.push('Temp');
console.log(otherObject.info); // ["Child", "Temp"]
console.log(childObject.info); // ["Child", "Temp"]
寄生式组合继承
方法:借用寄生式继承的思路,结合组合继承的方法,解决组合继承中需要调用两次父类构造函数的问题
原理:通过构造继承实现实例属性和实例方法的继承,通过寄生式继承实现原型属性和原型方法的继承
不用为了指定子类的原型而调用父类的构造函数,而是使用寄生式继承来继承父类的原型,然后指定给子类的原型
function SuperType(name, info) {
// 实例属性(基本类型)
this.name = name || 'Super';
// 实例属性(引用类型)
this.info = info || ['Super'];
// 实例方法
this.getName = function() { return this.name; };
}
// 原型方法
SuperType.prototype.getInfo = function() { return this.info; };
// 寄生式组合继承
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
function objectFactory(childType, superType) {
var prototype = object(superType.prototype); // 创建对象
prototype.constructor = childType; // 增强对象
childType.prototype = prototype; // 将父类原型指定给子类原型
}
function ChildType(name, info, message) {
SuperType.call(this, name, info);
this.message = message;
}
objectFactory(ChildType, SuperType);
寄生式组合继承是 JavaScript 中最常用的继承方式,ES6 中新增的 extends 底层也是基于寄生式组合继承的
原型事件
call、apply、bind 都是定义在函数原型上的,也就是说每个函数都能调用这些方法
那么它们都有什么作用呢?它们之间存在什么异同呢?下面让我们一起来探讨一下
call 事件
call 可以用于改变函数的执行环境,简单来说就是可以改变函数内部 this 的指向
使用 call 可以让一个对象借用另外一个对象的方法,可以借此实现继承
第一个传入的参数是上下文执行环境,即函数运行时 this 的指向,之后传入的参数将会直接传递给调用函数
在 call 调用完成后,返回调用函数的返回值
// 借用方法
let apple = {
color: 'red',
getColor: function() { return this.color }
};
let banana = {
color: 'yellow'
};
let color = apple.getColor.call(banana);
console.log(color);
// 执行结果:yellow
------------------------------------------------------------------------------------------------------------------------------
// 实现继承
function Parent(age, name) {
this.age = age;
this.name = name;
this.getName = function() { return this.name; };
this.setName = function(name) { this.name = name; };
}
function Child(age, name) {
Parent.call(this, age, name);
}
let child = new Child(18, 'Peter');
child.setName('Steve');
let name = child.getName();
console.log(name);
// 执行结果:Steve
apply 事件
apply 的作用与 call 完全一样,都能用于改变函数的执行环境,两者的区别仅仅在于传入的参数
第一个参数传入的都是上下文执行环境,即函数运行时 this 的指向,参数的区别在于之后传入的参数
之后传入的参数是调用函数执行所需的参数,call 是按照顺序直接传入,而 apply 是将参数放在数组中再传入
// 判断类型
let number = 0;
let string = '';
let boolean = true;
let object = {};
let array = [];
function typeOf(value) {
return Object.prototype.toString.apply(value).slice(8, -1);
}
console.log(typeOf(number)); // 执行结果:Number
console.log(typeOf(string)); // 执行结果:String
console.log(typeOf(boolean)); // 执行结果:Boolean
console.log(typeOf(object)); // 执行结果:Object
console.log(typeOf(array)); // 执行结果:Array
--------------------------------------------------------------------------------------------------------------------------------
// 数值求和
function addNumber() {
let isNumber = function(value) { return typeof value === 'number'; };
let numbers = Array.prototype.filter.apply(arguments, [isNumber]);
let sum = numbers.reduce(function(prev, curr) {
return prev + curr;
});
return sum;
}
let result = addNumber(1, 'a', 2, 'b', 3, 'c');
console.log(result);
// 执行结果:6
bind 事件
传入 bind 的参数与 call 完全相同,作用也与 call 大致一样,但它们还是有所区别的
call 在调用后马上执行函数,bind 不会,调用 bind 返回一个改变了上下文的新函数,可以在需要的时候再调用
// 借用方法
let apple = {
color: 'red',
getColor: function() { return this.color }
};
let banana = {
color: 'yellow'
};
let getColorForBanana = apple.getColor.bind(banana);
console.log(getColorForBanana); // 执行结果:ƒ () { return this.color }
let color = getColorForBanana();
console.log(color); // 执行结果:yellow
--------------------------------------------------------------------------------------------------------------------------------
// 解决回调函数 this 指向的问题
let object = {
value: 0,
asyncPrint: function() {
setTimeout(function() { console.log(this.value); }, 2000);
},
asyncPrintWithThat: function() {
let that = this;
setTimeout(function() { console.log(that.value); }, 2000);
},
asyncPrintWithBind: function() {
setTimeout(function() { console.log(this.value); }.bind(this), 2000);
}
};
object.asyncPrint(); // 执行结果:undefined
object.asyncPrintWithThat(); // 执行结果:0
object.asyncPrintWithBind(); // 执行结果:0