普通对象和函数对象#
在 JavaScript 中,一切皆对象。对象也分为普通对象和函数对象。
代码#
var o1 = {};
var o2 = new Object();
var o3 = new f1();
function f1() {}
var f2 = function () {};
var f3 = new Function();
console.log(typeof o1); // object
console.log(typeof o2); // object
console.log(typeof o3); // object
console.log(typeof f1); // function
console.log(typeof f2); // function
console.log(typeof f3); // function分析#
o1、o2、o3 是普通对象,f1、f2、f3 是函数对象。
区别:通过new Function()创建出来的对象都是函数对象,f1、f2 归根到底也是通过new Function()创建出来的。
构造函数#
对象归根到底都是通过函数创建出来的,是通过构造函数创建出来的。
代码#
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
alert(this.name);
};
}
person1 = new Person("Hale", 28, "engineer");
person2 = new Person("Bill", 26, "teacher");分析#
构造函数:任何通过 new 操作符来调用的函数,都可以称之构造函数,如 Person。
实例对象:通过构造函数创建的对象叫做实例对象,如 person1、person2。
原型对象#
任何函数都有一个 prototype 属性,它指向一个对象,这个对象就是原型对象。
代码#
function Person() {}
Person.prototype.name = "Hale";
Person.prototype.age = 28;
Person.prototype.job = "engineer";
Person.prototype.sayName = function () {
alert(this.name);
};
person1 = new Person();
person1.sayName(); // 'Hale';
person2 = new Person();
person2.sayName(); // 'Hale';
console.log(person1.sayName === person2.sayName); // true分析#
什么是原型对象?
Person.prototype就是函数 Person 的原型对象,它其实就是一个普通对象。
这个对象有三个属性(name,age,jon)和一个方法(sayName())。
原型对象有什么用?
主要作用是用来继承的,原型对象可以让特定类型的所有实例共享属性和方法。
继承#
代码#
function Person(name) {
this.name = name;
}
Person.prototype.job = "engineer";
Person.prototype.sayName = function () {
alert(this.name);
};
person1 = new Person("Hale");
console.log(person1.job); // engineer
person1.sayName(); // 'hale'
person2 = new Person("Amy");
console.log(person2.job); // engineer
person2.sayName(); // 'Amy'分析#
sayName是构造函数Person原型对象的方法。
通过构造函数创建的两个实例对象person1和person2虽然名字不一样,但是他们都继承了构造函数原型链的属性job和方法sayName。
隐式原型#
在最新的 ES 语法中,每一个对象都有一个__proto__属性,这个属性也是原型属性,但是和prototype有所区别,这个属性叫做“隐式原型”。为了区分,prototype叫做“显示原型”。
代码#
var f1 = new Function(); // 函数对象
var o1 = new Object(); // 普通对象
console.log(f1.__proto__ === Function.prototype); // true
console.log(o1.__proto__ === Object.prototype); // true分析#
通过构造函数生成的对象的__proto__(隐式原型)都指向它的构造函数的prototype(显示原型)。
构造器#
通过构造函数生成的对象都有一个constructor属性,这个属性叫做构造器。
代码#
var f1 = new Function(); // 函数对象
var o1 = new Object(); // 普通对象
console.log(f1.constructor === Function); // true
console.log(f1.__proto__.constructor === Function); // true
console.log(o1.constructor === Object); // true
console.log(o1.__proto__.constructor === Object); // true分析#
通过构造函数生成的实例对象的构造器指向它的构造函数,对象的隐性原型的构造器也指向它的构造函数。
原型链#
JavaScript 一切皆对象,我们还知道每一个对象都有隐式原型属性,指向它的构造函数的原型对象。那么构造函数的原型对象也是一个对象,那么它的隐式原型属性指向谁呢?
代码#
function Foo() {}
var f = new Foo();
console.log(f.__proto__ === Foo.prototype); // true
console.log(Foo.prototype.__proto__ === Object.prototype); // true分析#
我们发现构造函数的原型对象的隐式原型指向的是内置函数Object的原型对象。
如果再往上一层,又指向哪个呢?它的尽头在哪里?

如上图所示,通过__proto__属性,层层向上,就形成了一条“链子”,这条“链子”就是原型链,它的尽头是null。
原型链的形成是靠隐式原型__proto__属性的,而非显示原型prototype属性。当 JS 引擎执行对象的属性或者方法时,先查找对象本身是否存在该属性或者方法,如果不存在,会在原型链上的__proto__中,一级一级的往上查找,直到找到原型链的尽头null为止。
原型链继承#
代码分析#
function Animal() {}
Animal.prototype.sayName = function () {
alert("Animal");
};
function Dog() {}
Dog.__proto__ = Animal.prototype;
Dog.prototype.sayName = function () {
alert("Dog");
};
var dog = new Dog();
console.log(dog.sayName()); // Dog
console.log(Dog.sayName()); // Animal
console.log(Animal.sayName()); // 报错分析#
疑问:Dog.sayName()输出的结果为什么是Animal而不是Dog原型对象的方法中的Dog呢?
自己本身原型对象上就有方法,为什么还要用别的方法呢?
那是因为函数的prototype对象的作用只是提供(子类)继承使用的,本身无法使用 prototype 的属性和方法,用了会报错。
函数对象上的prototype不属于自身的原型链,它只是创建子类的核心,决定了继承的子类的数据类型,它在继承的子类的原型链上。而对象的__proto__才是记录着自己的原型链,决定了自身的数据类型,改变__proto__就等于改变对象的数据类型。
原型链的形成真正是靠__proto__而非prototype,是靠__proto__来“连接”的,“连接”的对象是“父类”的prototype。
现在回头看看原型对象的继承,其实也就是原型链的继承,只不过它是原型链继承的一小段而已。
继承改变#
代码#
function fn() {}
console.log(fn.__proto__ === Function.prototype); // true
console.log(fn instanceof Function); // true
console.log(fn instanceof Array); // false
fn.__proto__ = Array.prototype; // 改变__proto__指向
console.log(fn.__proto__ === Function.prototype); // false
console.log(fn instanceof Function); // false
console.log(fn instanceof Array); // true分析#
instanceof方法是用来检测对象的prototype是否存在于另外一个对象的原型链上。
JavaScript 内置的Function对象是专门用来创建各种函数的,所以所有函数的__proto__都指向Function的prototype的,而Array函数是用来创建数组的。
fn 本来是一个函数,它的__proto__指向的是Function的prototype,继承的是Function的prototype上的属性和方法,后面改变函数fn的__proto__指向,让它指向了函数Array的prototype,这样 fn 函数现在继承的是Array函数的prototype上的属性和方法。
这相当于直接改变了它的数据类型,由函数直接改成了数组,现在你可以测试下函数 fn 是否可以使用Array的prototype上的属性和方法。
hasOwnProperty#
私有属性和通过原型链继承的属性要怎么区分呢?通过hasOwnProperty方法来区分。
代码#
function Foo(name, age, job) {
// 私有属性和方法
this.name = name;
this.age = age;
this.job = job;
}
// 原型链的属性和方法
Foo.prototype.city = "Shenzhen";
Foo.prototype.sayName = function () {
alert(this.name);
};
f = new Foo("hale", 28, "engineer");
for (key in f) {
console.log(key, f[key]); // 打印所有的属性和方法
}
for (key in f) {
if (f.hasOwnProperty(key)) {
console.log(key, f[key]); // 只打印私有属性和方法
}
}分析#
使用for ... in可以把对象的所有的属性都遍历出来,不管是私有的属性方法还是有继承的属性方法。但是使用hasOwnProperty之后,只显示私有属性和方法,不显示通过原型链继承的属性和方法。
ES6 的类#
ES6 引入了类(class)的语法,但是它的本质上还是原型。
代码#
class Animal {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
class Dog extends Animal {
constructor(name, age) {
super(name);
this.age = age;
}
speak() {
return "woof";
}
}
const dog = new Dog("wang", 3);
console.log(
dog.age + " year old dog " + dog.getName() + " says: " + dog.speak()
); // 3 year old dog wang says: woof分析#
JavaScript 语言没有像其他语言一样使用类和对象的概念,而是使用了原型克隆,ES6 的类的本质上还是原型,只不过换了一种写法而已,相当于原型的语法糖。
类里面的constructor相当于构造函数的constructor,然后继承使用了extends关键字,创建实例还是使用new,this指向的就是new出来的实例。其实类本质上就是一个构造函数,可以往它的prototype上添加属性和方法,然后new出来的实例还是会继承类的原型上的方法的,这些都是和构造函数完全一样的。
总结#
原型的几个重要特性
- 任何函数都有一个
prototype属性,它指向一个对象,就是原型对象,也是一个普通对象。 - 任何通过构造函数生成的实例对象都有一个
__proto__属性,它也是一个普通对象。 - 任何对象的
__proto__属性指向它的构造函数的prototype属性。 - 函数的
prototype不属于自身的原型链,它是创建子类的核心,决定了子类的数据类型。 - 而对象的
__proto__记录着自己的原型链,决定了自身的数据类型,改变__proto__就等于改变对象的数据类型。 - 原型链的形成真正是靠
__proto__而非prototype。 - 子类的
__proto__和父类的__proto__记录的原型链是两条不一样的原型链。