JavaScript原型和原型链知识梳理

1,118 阅读8分钟

最近学习了原型和原型链,来做一次总结加深理解,同时也沿着自己的思路看看是否有误,如有表述不当或者错误的地方,欢迎指正,欢迎交流😊

以下笔者先按原型以及原型相关的属性来解释原型和原型链,再讲述他们之间的关系。

原型的设计思想

由于之前一直学习的是强类型的语言,对类和对象有一个固定的理解,当学习JavaScript的时候以为js的类和对象机制也同其他强类型的语言相同,深入学习后发现其实不然。

Brendan Eich在开发JavaScript的时候其实考虑了是否采用完整的面向对象的思想(类&继承),但考虑到JavaScript作为一种简易的脚本语言,并且为了降低学习门槛。

由原型的构造函数来生成实例对象,构造函数可以初始化实例对象的属性和方法,而各实例对象继承原型的属性和方法(公有:值相同,是同一个属性和方法),但各实例对象占据独立的内存,其属性和方法私有(”私有“:值可以相同,但不是同一个属性和方法)

为了便于表述,笔者用私有来表达实例对象各自拥有的属性和方法是不合适的,私有应为外部不能直接访问,需要通过公有方法来访问。

继承同理,继承应为类与对象的关系所拥有

1. prototype

1.1 基本概念

每个构造函数(约定俗成函数名大写的函数为构造函数,与普通函数无异)都具有一个prototype属性,所有实例对象的公有属性和方法,都存放在prototype所指向的对象中——我们称之为原型,而那些私有的属性和方法由构造函数来生成

实例对象被创建(new)后,自动继承原型对象所拥有的属性和方法

我们创建一个构造函数,来查看他的属性和方法

function Person() { }
console.dir(Person);

1.2 继承关系

  • 公有属性和方法

    实例对象继承原型对象的属性

    Person.prototype.name = '小明'
    Person.prototype.say = function() {
      console.log('hahahah');
    }
    function Person() {}
    var person = new Person()
    console.log(person.name); // 小明
    console.log(person.say()); // hahahah
    

    如果构造函数拥有该属性,则不会向上查找该属性,同时,修改构造函数属性和方法不会影响原型的属性和方法

    Person.prototype.name = '原型对象的小明'
    Person.prototype.type = 'student'
    function Person() {
        this.name = '实例对象的小明'
    }
    var person = new Person()
    person.type = 'worker'
    console.log(person.name); // 实例对象的小明
    console.log(person);// Person {name: "实例对象的小明", type: "worker"}
    console.log(person.type); // worker
    

    构造函数初始化实例对象的属性

    function Person(name) {
        this.name = name
    }
    var person = new Person('自定义名字:小明')
    console.log(person.name); // 自定义名字:小明
    
  • 私有属性和方法

    修改实例对象自身的属性不影响其他实例对象的属性

    Person.prototype.name = '小明'
    function Person() {
        this.phone = 'iphone11'
    }
    var person1 = new Person()
    var person2 = new Person()
    person1.phone = '小米'
    console.log(person1.phone); // 小米
    console.log(person2.phone); // iphone11
    

1.3 作用

  1. 访问构造函数的原型对象

  2. 提取公有属性和方法

    // 提取公有属性
    Car.prototype.name = 'BBMW'
    Car.prototype.lang = 4900
    Car.prototype.height = 1400
    function Car(color, owner) {
      this.color = color
      this.owner = owner
      // 重复,耦合度高,代码复用性低
      // this.name = 'BMW'
      // this.lang = 4900
      // this.height = 1400
    }
    
    var car = new Car('red', 'tim')
    var car1 = new Car('green', 'sandy')
    
    console.log(car.name, car1.lang);
    
  3. 操作原型链中的增删改查(接下来会解释)

2. constructor属性

1.1 基本概念

  • 实例对象和实例对象的原型对象可以通过constructor访问构造函数

    function Person() {}
    var person = new Person()
    console.log(person.constructor);  // ƒ Person() {}
    console.log(Person.prototype.constructor);// ƒ Person() {}
    console.log(person.constructor === Person.prototype.constructor); // true
    

但是注意❗:这里的访问是指可以打印出构造函数,但不能通过constructor属性来访问构造函数内部的属性和方法,我们只能通过实例对象来访问构造函数内部的属性和方法

function Person() { 
    this.name = 'angel'
}
var person = new Person()
console.log(person.name); // angel
console.log(person.constructor.name);  // Person
console.log(Person.prototype.constructor.name);// Person
console.log(person.constructor === Person.prototype.constructor); // true
  • 改变constructor的作用

    给constructor赋值的时候,constructor变成了普通的属性

    Person.prototype.constructor =  'male'
    function Person() { 
        this.constructor = 'female'
    }
    var person = new Person()
    console.log(person.constructor);  // female
    console.log(Person.prototype.constructor);// male
    console.log(person.constructor === Person.prototype.constructor); // false
    

1.2 拓展

普通对象也具有constructor属性

var obj = {
  name: 'jack'
}
console.log(obj.constructor === Object); // true

从上面的运行结果得知用对象字面量创建的对象是由Object构造函数来创建的

var obj = {}
// 两者相同
var obj = new Object()

3. __proto__属性

所有对象(构造函数也是对象)都具有__proto__属性,我们称之为对象原型,更确切来说是隐式原型,同prototype一样可以访问的原型,区别在于prototype是构造函数所拥有的属性,而__proto__是所有对象都拥有的属性

1.1 new操作符的执行过程

function Person() {}
var person = new Person()

当我们实例化对象的时,在构造函数内部发生了如下操作:

function Person() {
    // 当构造函数发生new操作时,先在构造函数内部创建了一个空对象
    // 使__proto__指向构造函数的原型
    // 返回这个对象
    var this = {
        __proto__: Person.prototype
    }
    return this
}
var person = new Person()

所以当我们打印 console.log(person.__proto__ === Person.prototype);

结果为true

回到2.1.2 我们可以推断出

var obj = {
  name: 'jack'
}
console.log(obj.constructor === Object); // true
console.log(obj.__proto__ === Object.prototype);// true

1.2 修改原型

构造函数的原型对象和实例的原型对象是相同的,那么我们也可以为他们指定不同的原型

function Person() { }
var person = new Person()
var obj = {
    name: '小明'
}
person.__proto__ = obj
console.log(person.__proto__); // {name: "小明"}
console.log(Person.prototype );// {type: "male", constructor: ƒ}

此时person实例对象的原型是对象obj,再通过Person() new出来一个新的实例对象与person实例对象也不再产生联系

如果修改Person.prototype,则保留了原型链的完整性,new出来的所有实例对象都会继承obj

Person.prototype.type = 'male'
function Person() { }
var obj = {
  name: '小明'
}
Person.prototype = obj
var person = new Person()
console.log(person); // 其原型具有name属性
console.log(person.__proto__);  // {name: "小明"}
console.log(Person.prototype);  // {name: "小明"}
var person1 = new Person()
console.log(person1); // 其原型具有name属性

4. 原型链

从上面的分析我们可以得出每个构造函数都有prototype显示指向原型对象,而原型对象可以通过constructor指向构造函数,构造函数的实例又可以通过__proto__隐式指向原型对象

那么,如果让原型对象指向另一种构造函数的实例,就会使得该原型对象指向另一种原型对象,就像一条链子一样把原型关系串起来,我们就称其为原型链

Adult.prototype.work = 'musician'
function Adult() { }
Teenager.prototype = new Adult()
function Teenager() {
    this.hobbit = {
        music: 'rock'
    }
}
Child.prototype = new Teenager()
function Child() {
    this.name = 'tim'
}
var child = new Child()
console.log(child.__proto__); // Adult {hobbit: {…}}
console.log(child.__proto__.__proto__); // Adult {}
console.log(child.__proto__.__proto__.__proto__);// {work: "musician", constructor: ƒ}
console.log(child.__proto__.__proto__.__proto__.__proto__); // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}

他们的关系如下:

1.1 在原型链中的增删改查

  • 查:逐级查询

    function Teenager() {
        this.name = 'timy'
        this.hobbit = {
            music: 'rock'
        }
    }
    Child.prototype = new Teenager()
    function Child() {
        this.name = 'tim'
    }
    var child = new Child()
    console.log(child.name); // tim 如果构造函数没有则往上查询
    
  • 删:实例对象可以删除自身的属性,但不可以删除原型对象的属性

    Teenager.prototype.type = 'student'
    function Teenager() {
        this.name = 'timy'
        this.hobbit = {
            music: 'rock'
        }
    }
    var teenager = new Teenager()
    Child.prototype = teenager
    function Child() {
        this.name = 'tim'
    }
    var child = new Child()
    console.log(Teenager.prototype.type); // student  原型对象的type
    console.log(child.type); // student  逐级查找type
    delete child.type
    console.log(child.type); // student  删除失败
    console.log(child); // Child类型的实例对象child  可以查看原型链,在原型链上仍然可以找到type
    

    删除原型链的属性和构造函数的属性:

    delete Teenager.prototype.type
    console.log(child.type);// undefined 本应该报错,但是child实例对象在查找不到该属性时会自动生成一个type属性,值为undefined
    delete child.name // 删除实例对象的name属性
    console.log(child.name); // timy  逐级查找name
    delete Child.prototype.name // 删除实例对象的构造函数的name属性
    console.log(child); // 查看原型链,无name属性
    
  • 增&改:

    Teenager.prototype.type = 'student'
    Teenager.prototype.name = 'mike'
    function Teenager() {
        this.name = 'timy'
        this.hobbit = {
            music: 'rock'
        }
    }
    var teenager = new Teenager()
    Child.prototype = teenager
    function Child() { }
    var child = new Child()
    child.age = 18 // 增加实例对象的name属性
    Child.prototype.name = 'anna' // 修改构造函数的name
    Teenager.prototype.type = 'worker' // 修改原型对象的type
    Child.prototype.__proto__.type = 'free-worker'// 修改原型对象的type
    console.log(child.name); // anna 如果没有设置则打印timy
    console.log(child.age); // 18 添加在实例对象上的属性,原型对象不会添加该属性
    console.log(child.type); // free-worker (最终修改的结果)
    

1.2 实例可以修改(增加或更改)原型的属性吗?

答案是可以修改引用类型

Teenager.prototype.type = 'student'
Teenager.prototype.name = 'mike'
function Teenager() {
    this.name = 'timy'
    this.hobbit = {
        music: 'rock'
    }
}
var teenager = new Teenager()
Child.prototype = teenager
function Child() { }
var child = new Child()

child.hobbit.sport = 'basketball' // hobiit是引用类型,可以增加属性
child.hobbit.music = 'pop' // hobiit是引用类型,可以修改属性

console.log(child);

打印结果:

5. 基本类型和函数的原型

初次学习时我思考,既然对象具有__proto__隐式原型,那么构造函数也是对象,它具有__proto__隐式原型吗?

验证一下

function Person() {}
console.log(Person.__proto__); // [Function]

我们到控制台打印以下发现是一个空函数

所有基本类型的构造函数和函数都指向Function.prototype,它是一个空函数(Empty function)

Number.__proto__ === Function.prototype // true
Boolean.__proto__ === Function.prototype // true
String.__proto__ === Function.prototype // true
Object.__proto__ === Function.prototype // true
Function.__proto__ === Function.prototype // true
Array.__proto__ === Function.prototype  // true
RegExp.__proto__ === Function.prototype // true
Error.__proto__ === Function.prototype  // true
Date.__proto__ === Function.prototype  // true

如果我们继续溯源,Function.prototype__proto__又是谁呢?

console.log(Function.prototype.__proto__)

结果

{constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …}

其实就是Object.prototype

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

Object.prototype__proto__又是谁呢?是null,到达原型链顶层。

6. 图解概括原型链

经过上面的所有解释,想必已经对原型和原型链有一个清晰的了解了

再来看看这张图,涵盖了我所讲的内容

参考

  1. Javascript继承机制的设计思想——阮一峰
  2. JavaScript中__proto__与prototype的关系——snandy