js核心系列(六)—— 原型和原型链知多少

2,855 阅读7分钟

js核心系列流程图.png

话题引入

首先我们声明一个普通的对象

let obj = {name:"obj"}

我们在控制台输入 obj.valueOf(),obj.toString(),onj.constructor

366666.png

在我们没有对 obj 进行任何其他操作之前,发现 obj 已经有几个属性(方法)了

我们接着在打印一下obj这个对象

9999.png

是不是感觉很奇怪,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

实例原型.png
原型定义:

每一个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

11.png

constructor

每个原型对象都有一个constructor 属性指向关联的构造函数。

function Person() {

}
console.log(Person === Person.prototype.constructor); // true

33.png

以上就是 构造函数,原型和实例对象之间的关系。

综上我们已经可以得出:

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 ,所以我们在更新下关系图 333.png

原型链

那 Object.prototype 的原型呢?是null,我们可以打印

console.log(Object.prototype.__proto__ === null) // true

所以查找属性的时候查到 Object.prototype 就可以停止查找了。null没有原型。
下图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。 原型链.png

构造函数的原型

看到上面的关系图,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 的实例。

image.png

经典图终于可以放出来了

如果,你看懂了这个图,说明你已经基本掌握了原型和原型链的知识了。 image.png

再来一张彩色的

54863965-6b262b80-4d8a-11e9-906a-f012204942e5.png

补充

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 对象

  1. Student 继承Person
  2. Person 包含一个实例变量 name, 包含一个方法 printName
  3. Student 包含一个实例变量 score, 包含一个实例方法printScore
  4. 所有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