1. 复杂的原型链
原型是 JavaScript 向面向对象编程语言进化的产物。
为什么要面向对象编程?为了代码复用,为了抽象出类似的概念,为了让代码更有逻辑,更符合我们本身的世界观。
在我们的意识中,其实世界的事物都是一个一个对象。例如,男人和女人,都属于人的一个子分支,他们两者都具有人的一些共同点特点,或者说,男人和女人这两个对象都“继承”了“人”这个对象的一些特点,而又有各自的不一样属性。或者说:人,就是男人和女人的原型。
在 JavaScript 中,Function
,Object
分别是函数,对象的原型,而其实函数也是一个特殊的对象,因此函数原型创作的函数也是特殊的对象。
var fun =function() {
console.log('this is a function');
};
var obj = {
string: 'obj',
};
console.log(Function.prototype.isPrototypeOf(fun));
console.log(Object.prototype.isPrototypeOf(fun));
console.log(Function.prototype.isPrototypeOf(obj));
console.log(Object.prototype.isPrototypeOf(obj));
console.log(Object.prototype.isPrototypeOf(Function));
// true, true, true, false, true
利用Object.prototype.isPrototypeOf(targetObj)
方法,可以检测Object
测试对象是否在检测目标的原型链上,根据输出可以看到,Function
对象和 Object
对象在所有函数的原型链上,其中Object
是Function
的原型,而Object
是所有对象的原型,包括 Function
对象。
我们可以使用一个 ES5 的方法来获取一个对象的原型:
Object.getPrototypeOf(obj)
var Person = function (name) {
this.name = name;
};
var alice = new Person('alice');
console.log(Object.getPrototypeOf(alice));
console.log(Object.getPrototypeOf(alice) === Person.prototype);
// Person.prototype, true
可以看到,Person
原型,生成了一个实例 alice
,因此Person
对象 是alice
的原型。
类似的,Person
也应该有原型,所谓原型,就是生成它的那个角色。上面实例中,Person
是一个函数,那它的原型是什么呢?我们用代码探究:
console.log(Object.getPrototypeOf(Person));
console.log(Object.getPrototypeOf(Person) === Function.prototype);
// ƒ () { [native code] } , true
由于创造函数的函数(Function.prototype)是 JavaScript 语言的源代码,因此不存在它是什么,但是我们通过第二个判断得知(三个等号),生产函数的函数就是Function.prototype
。
而 Function.prototype
本身是一个对象,因此它也是有原型的,而它的原型就是创造对象的对象,Object.prototype。
console.log(
Object.getPrototypeOf(Function.prototype) === Object.prototype);
// true
那 Object.prototype
有没有原型呢?有的,为了避免原型链没有尽头,ES 规定了Object.prototype的原型就是 null
。
因此我们可以画出原型链:
alice -> Person -> Function -> Object -> null
原型链之所以是复杂的,就是因为在代码构建工程的过程中,可以会产生无数的原型和原型生成的实际对象,而原型可能又会有它的原型,等等,一层层的关系形成一个链条,这就是原型链。
但是原型链的灵活也在于此:一个实例可以调用它的原型的方法,以及原型的原型的方法。
也就是它在使用函数时,会在自身的属性/方法中查找,如果没有找到该方法(函数),它就会沿着原型链,查找它的原型中的方法,如果仍没有,继续沿原型链进行查找,知道原型链的尽头:null
。
可以看到,所有的对象都是Function
和Object
原型的下游,因此,所有的函数生产对象都可以调用 Function.prototype
和Object.prototype
中的方法,call(this, arg1,arg2,...), apply(this,arg[]), bind(this), toString(), isPropertyOf(), hasOwnProperty()
2. 构造函数与 new 命令
其实上面我们已经涉及到了构造函数。构造函数的实现没有脱离原型链这一基础,只是在语法实现上来说,更靠近其他语言例如 Java 的写法。一个典型的构造函数:
function Person(name, email) {
this.name = name;
this.email = email;
this.sayName =function() {
console.log(this.name);
};
this.sayEmail = function() {
console.log(this.email);
};
}
一个典型的构造函数,有它的属性,name, email
,有它的方法,sayName(), sayEmail()
,他的实例都会具有它的这些方法 由它可以生出很多实例,但是需要配合 一个关键词使用:new
,这也是借鉴 Java 语言中的写法。
// Java class
public class Dog {
string name;
int age;
string color;
public barking() {
System.out.println("wo-wo-wo");
}
}
Dog myDog = new Dog();
myDog.barking();
类似的,在 JavaScript 中,构造函数就相当于是 Java 中的类,它们都可以生成一个实例,并且实例可以调用构造函数中的方法。
var alice = new Person('alice', 'alice@google.com');
alice.sayName();
alice.sayEmail();
// "alice" "alice@google.com"
这其中涉及到 new 运算符的实现,大致的实现步骤来说应该是这样:
- 生成一个新的空对象。
- 将1.中生成的空对象的原型指向构造函数的
prototype
属性。 - 将这个空对象赋值给函数内部中的this关键字。
- 开始执行构造函数内部的代码。
- 执行完毕之后,如果原构造函数没有返回其他对象,则返回
this
。
此时的this
值就指向当前的实例,本例中就是alice
。
面试有可能会面的:实现一个 new 命令。
根据上面的四步,实现一个 new 命令:
// constructor 是构造函数,params是传入构造函数的参数
function _new (constructor, params) {
var args = [...arguments]; // 将传入参数变成数组
var constructor = args.shift(); // 取出构造函数
var context = Object.create(contructor.ptototype);
/* 以构造函数的prototype为原型,创建一个prototpye对象,新建的对象具有所有构造函数的prototype的属性和方法。 */
var result = constructor.apply(context, args); //执行构造函数
return (typeof result === 'object' && result!==null) ? result : context;
// 如执行结果非空且类型为对象,则返回该结果,否则,返回新创建的对象。
}
3. ES6 的 class
ES6 引入的 class 类的定义方法,从语法上更靠近了基于类的编程语言,实际上它的运行机制仍是基于原型的。
class Person {
constructor(name, email) {
this.name = name;
this.email = email;
}
// Methods
sayName () {
console.log(this.name);
};
sayEmail() {
console.log(this.email);
};
}
在 class
的语法中,基本的变量数据放到 constructor
构造数据中,而其他的成员函数,可以直接写成函数的形式,不需要function
关键词,调用的时候直接在前面加上this.
。
class
还提供常用的getter
和 setter
可以使用。因此,setter
,getter
配合methods
可以令function
+new
的奇怪组合消失了,让上帝的归上帝,凯撒的归凯撒,function
回归到它定义函数的本意,而不是定义一个'类'(虽然实际上仍是原型构造函数)。
set setJob(offer) {
this.offer = offer;
}
get job() {
return this.upperName();
}
upperName() {
return this.offer.toUpperCase();
}
var alice = new Person('alice', 'alice@google.com');
alice.setJob = 'Front-end';
console.log(alice.job); // "FRONT-END"
而且class
的继承也更像继承了。一个子类可以继承父类,直接使用extends
关键字,就像在别的基于类的函数(Java, C++)一样。
class Woman extends Person {
constructor(name, mail) {
super(name, mail);
this.name = name;
this.mail = mail;
this.organ = 'vaginal';
}
specialOrgan () {
console.log(this.name + ' has '+ this.organ);
}
}
var alice = new Woman('alice', 'alice@google.com');
alice.setJob = 'Front-end';
console.log(alice.job);
alice.specialOrgan();
// "FRONT-END" "alice has vaginal"
在继承的子类Woman
中,在调用父类中的成员数据前,需要调用super
关键词包括所有需要调用的数据,否则会出现引用错误。上面代码可以看到,子类继承了父类的setter
和getter
,而且子类有自己的方法specialOrgan()
,并且在子类的方法中成功地调用了父类的name
属性。
5. 参考阅读
- javascript-the-core,Dmitry Soshnikov。
- 实例对象与 new 命令,阮一峰。
- extends,MDN。