原型链、构造函数
理解:
- 因为 f1.__proto__ === Foo.protoype 所以 Foo.prototype.constructor === f1.__prototype__.constructor === Foo
- 因为构造函数的 constructor 是一个函数,所以构造函数 Foo、Object、Function 的 constructor 都指向 Function
- prototype 是一个对象,因此 xx(Object 除外).prototype.__proto__ === Object.prototype
- Object.prototype.__proto__ === null
- Object() 也是一个函数实例,因此 Object.__porto__ === Function.prototype
总结一下:
- 我们需要牢记两点:
- proto和 constructor 属性是对象所独有的;
- prototype 属性是函数所独有的,因为函数也是一种对象,所以函数也拥有proto和 constructor 属性。
- proto属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的proto属性所指向的那个对象(父对象)里找,一直找,直到proto属性的终点 null,再往上找就相当于在 null 上取值,会报错。通过proto属性将对象连接起来的这条链路即我们所谓的原型链。
- prototype 属性的作用就是让该函数所实例化的对象们都可以找到公用的属性和方法,即 f1.proto === Foo.prototype。
- constructor 属性的含义就是指向该对象的构造函数,所有函数(此时看成对象了)最终的构造函数都指向 Function。
注意:
Object.prototype.toString() 与(Number| String| Array|Date …).prototype.toString() 区别:
- 前者可以用于检测数据类型 Object.prototype.toString.call(arr)
- 后者用于将调用者转化为字符串格式:arr.toString() | Array.prototype.toString.call(arr)
new 关键字
new关键字让构造函数具有了与普通函数不同的许多特点,new的过程中,执行了如下过程:
function trivialNew(constructor, ...args) {
var o = {}; // 创建一个对象
constructor.apply(o, args);
return o;
}
这并不是 new 的完整实现,因为它没有创建原型(prototype)链。想举例说明 new 的实现有些困难,因为你不会经常用到这个,但是适当了解一下还是很有用的。在这一小段代码里,…args(包括省略号)叫作剩余参数(rest arguments)。如名所示,这个东西包含了剩下的参数。
var bill = trivialNew(Person, "William", "Orange");
// 等价于
var bill = new Person("William", "Orange");
复杂实现
// 先一本正经的创建一个构造函数,其实该函数与普通函数并无区别
var Person = function(name, age) {
this.name = name;
this.age = age;
this.getName = function() {
return this.name;
}
// return this
}
// 将构造函数以参数形式传入
function New(func) {
// 声明一个中间对象,该对象为最终返回的实例
var res = {};
if (func.prototype !== null) {
// 将实例的原型指向构造函数的原型
res.__proto__ = func.prototype;
}
// ret为构造函数执行的结果,这里通过apply,将构造函数内部的this指向修改为指向res,即为实例对象
var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
// 当我们在构造函数中明确指定了返回对象时,那么new的执行结果就是该返回对象
if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
return ret;
}
// 如果没有明确指定返回对象,则默认返回res,这个res就是实例对象
return res;
}
// 通过new声明创建实例,这里的p1,实际接收的正是new中返回的res
var p1 = New(Person, 'tom', 20);
console.log(p1.getName());
// 当然,这里也可以判断出实例的类型了
console.log(p1 instanceof Person); // true
继承
构造函数的继承
可以借助call/apply来实现
var Person = function(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.getName = function() {
return this.name;
}
Person.prototype.getAge = function() {
return this.age;
}
var Student = function(name, age, grade) {
// 通过call方法还原Person构造函数中的所有处理逻辑
Person.call(this, name, age);
this.grade = grade;
}
// 等价于var Student = function(name, age, grade) {
this.name = name;
this.age = age;
this.grade = grade;
}
原型的继承
原型的继承则稍微需要一点思考。首先我们应该考虑,如何将子类对象的原型加入到原型链中?我们只需要让子类对象的原型,成为父类对象的一个实例(son.__proto__ === perent.prototype),然后通过__proto__就可以访问父类对象的原型。这样就继承了父类原型中的方法与属性了。
因此我们可以先封装一个方法,该方法根据父类对象的原型创建一个实例,该实例将会作为子类对象的原型。
function create(proto, options) {
// 创建一个空对象
var son = {};
// 让这个新的空对象成为父类对象的实例
son.__proto__ = proto;
// 传入的方法都挂载到新对象上,新的对象将作为子类对象的原型
Object.defineProperties(son, options); // Object.defineProperties方法直接在一个对象上定义新的属性或修改现有属性,并返回该对象。
return son;
}
简单封装了create对象之后,我们就可以使用该方法来实现原型的继承了。
Student.prototype = create(Person.prototype, {
// 不要忘了重新指定构造函数
constructor: {
value: Student
}
getGrade: {
value: function() {
return this.grade
}
}
})
在ECMAScript5中直接提供了一个Object.create方法来完成我们上面自己封装的create的功能。因此我们可以直接使用Object.create()
function Person(name, age) {
this.name = name;
this.age = age;}Person.prototype.getName = function() {
return this.name
}
Person.prototype.getAge = function() {
return this.age;
}
function Student(name, age, grade) {
// 构造函数继承
Person.call(this, name, age);
this.grade = grade;
}
// 原型继承Student.prototype = Object.create(Person.prototype, {
// 不要忘了重新指定构造函数
constructor: {
value: Student
}
getGrade: {
value: function() {
return this.grade
}
}
})
var s1 = new Student('ming', 22, 5);
console.log(s1.getName()); // ming
console.log(s1.getAge()); // 22
console.log(s1.getGrade()); // 5