话题引入
首先我们声明一个普通的对象
let obj = {name:"obj"}
我们在控制台输入 obj.valueOf(),obj.toString(),onj.constructor
在我们没有对 obj 进行任何其他操作之前,发现 obj 已经有几个属性(方法)了
我们接着在打印一下obj这个对象
是不是感觉很奇怪,valueOf / toString / constructor等这些方法或者属性是怎么来?我们并没有给 obj声明这些东西呀。想知为何,请听下面慢慢介绍。
构造函数介绍
其实构造函数也就只是一个普通的函数而已,如果这个函数可以使用
new
关键字来创建它的实例对象,那么我们就把这种函数称为构造函数。构造函数首字母一般大写。
实例成员: 就是在构造函数内部,通过this添加的成员。实例成员只能通过实例化的对象来访问。
静态成员: 在构造函数本身上添加的成员,只能通过构造函数来访问
function Person() {
// 实例成员
this.name = "jimmy";
this.age = 18;
}
// 静态成员
Person.sex = '男';
let person = new Person(); // {name:'jimmy',age:18}
console.log(person.name) // jimmy
console.log(person.sex); // undefined 实例无法访问sex属性
console.log(Person.name); // Person 构造函数无法直接访问实例成员
console.log(Person.sex); // 男 构造函数可直接访问静态成员
在这个例子中,Person 就是一个构造函数,我们使用new关键字可以得到实例对象person。
prototype
每个 函数 都有一个 prototype 属性。
function Person() {
}
// 注意 prototype 是函数才会有的属性
Person.prototype.name = 'Kevin';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Kevin
console.log(person2.name) // Kevin
原型定义:
每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。 上例中name就是继承至Person的原型对象(Person.prototype)
这里你就知道了,文章最开始打印的obj对象里面的属性就是从它的原型继承来的,并且var obj = {} 等同于 var obj = new Object()
__ proto __
每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型
function Person() {
}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true
constructor
每个原型对象都有一个constructor 属性指向关联的构造函数。
function Person() {
}
console.log(Person === Person.prototype.constructor); // true
以上就是 构造函数,原型和实例对象之间的关系。
综上我们已经可以得出:
function Person() {
}
var person = new Person();
console.log(person.__proto__ === Person.prototype) // true
console.log(Person.prototype.constructor === Person) // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
实例对象与原型
当读取实例对象的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
function Person() {
}
Person.prototype.name = 'jimmy';
let person = new Person();
person.name = 'chimmy';
console.log(person.name) // chimmy
delete person.name;
console.log(person.name) // jimmy
在这个例子中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 chimmy。但是当我们删除了 person 的 name 属性时,再来读取 person.name时,此时从 person 对象中找不到 name 属性,然后就会从 person 的原型也就是 person.__ proto __ ,也是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 jimmy。
但是万一还没有找到呢?原型的原型又是什么呢?
原型的原型
其实原型的原型一直到最后是通过 Object 构造函数生成的,结合之前所讲,实例的 __ proto__ 指向构造函数的 prototype ,所以我们在更新下关系图
原型链
那 Object.prototype 的原型呢?是null,我们可以打印
console.log(Object.prototype.__proto__ === null) // true
所以查找属性的时候查到 Object.prototype 就可以停止查找了。null没有原型。
下图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
构造函数的原型
看到上面的关系图,Person这个构造函数的原型又是什么呢?
我们上面提到过,每一个JavaScript对象(除了 null )都具有的一个属性,叫__proto__,这个属性会指向该对象的原型。由于构造函数Person是由 Function函数生成出来的,所以得出下面的结论:
// 构造函数 Person
function Person(){
}
Person.__proto__ === Function.prototype
Function.prototype的原型是 Object.prototype
Function.prototype.__proto__ === Object.prototype
Object.prototype.__prototo__ === null 这里原型链就结束了。null没有原型
所有的函数可以使用 new Function()
的方式创建,所以所有函数(包括自带的构造函数)都是 Function
的实例。
经典图终于可以放出来了
如果,你看懂了这个图,说明你已经基本掌握了原型和原型链的知识了。
再来一张彩色的
补充
constructor
function Person() {
}
var person = new Person();
console.log(person.constructor === Person); // true
当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:person.constructor === Person.prototype.constructor
真的是继承吗?
每一个对象都会从原型继承属性,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是: 继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。
原型、原型链的意义何在
看到这里我们思考一下,原型、原型链的意义何在?
原型对象的作用,是用来存放实例中共有的那部份属性、方法,可以大大减少内存消耗。 用我们文章开始的Person构造函数和person实例对象举例说:
function Person(name,age){
this.name = name;
this.age = age;
this.say:()=>{
console.log("我会说话")
}
}
let person1 = new Person("小明",20);
let person2 = new Person("小红",18)
console.log(person.say===person2.say) // false
很明显,person1 和 person2 指向的不是一个地方。 所以在构造函数上通过this来添加方法的方式来生成实例,每次生成实例,都是新开辟一个内存空间存方法。这样会导致内存的极大浪费,从而影响性能
那我们通过原型来添加方法
function Person(name,age){
this.name = name;
this.age = age;
}
Person.prototype.say = () => {console.log("我会说话")}
let person1 = new Person("小明",20);
let person2 = new Person("小红",18)
console.log(person.say===person2.say) // true
原型对象存放了person1、person2共有的方法say。 我们不用在生成的每个实例上都生成这个方法,而是将这一属性存在他们的构造函数原型对象上,对于人类Person这样的构造函数。相同的属性、方法还有很多很多,比如我们是黑头发,我们都有吃,睡这样一个方法,当相同的属性、方法越多,原型、原型链的意义越大。存放在原型对象上那些共有的属性、方法,可以大大减少内存消耗。
检验一下自己吧
1.下面打印什么
Object.prototype.__proto__
Function.prototype.__proto__
Object.__proto__
2.下面打印什么
function A(){};
A.prototype.n = 1;
var b = new A();
A.prototype = {
n: 2,
m: 3
}
var c = new A();
console.log(b.n);
console.log(b.m);
console.log(c.n);
console.log(c.m);
3.下面打印什么
function F() {};
Object.prototype.a = function () {
console.log("a");
};
Function.prototype.b = function () {
console.log("b");
};
var f = new F();
F.a();
F.b();
f.a();
f.b();
4.按照如下要求实现Person 和 Student 对象
- Student 继承Person
- Person 包含一个实例变量 name, 包含一个方法 printName
- Student 包含一个实例变量 score, 包含一个实例方法printScore
- 所有Person和Student对象之间共享一个方法
答案在下面
答案
Object.prototype.__proto__ //null
Function.prototype.__proto__ //Object.prototype
Object.__proto__ //Function.prototype
console.log(b.n); // 1
console.log(b.m); // undefined
console.log(c.n); // 2
console.log(c.m); // 3
F.a(); // a
F.b(); // b
f.a(); //a
f.b(); // f is not a function
f是一个对象,f的__proto__指向F.prototype,F.prototype.__proto__指向Object.prototype,所以f 可以取到a方法, 由于f的原型链上没经过Function.prototype,所以取不到b方法。
由于构造函数F是由Function new出来的,所以F.__proto__指向Function.prototype,所以F函数可以取到b方法
function Person (name){
this.name = name;
this.printName=function() {
console.log('This is printName');
};
}
Person.prototype.commonMethods=function(){
console.log('我是共享方法');
};
function Student(name, score) {
Person.call(this,name);
this.score = score;
this.printScore=function() {
console.log('This is printScore');
}
}
Student.prototype = new Person();
let person = new Person('小紫',80);
let stu = new Student('小红',100);
console.log(stu.printName===person.printName);//false
console.log(stu.commonMethods===person.commonMethods);//true