详解JS原型与原型链(长文详解)

原型一直是js的核心知识点,虽然自从es6的class语法糖问世之后,已经不是必用的技术,但对于资深前端来说,仍然是必须要了解的知识点。话不多说,直接开始

构造函数

工厂模式已经弃用,此处略过,我们先来回顾一下构造函数模式:

function Person(name, age, job) {
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = function() { alert(this.name) } 
}
var person1 = new Person('Nicholas', 28, 'Software Engineer');
var person2 = new Person('Greg', 23, 'Doctor');
复制代码

要创建 Person 的新实例,需要经过四个步骤:

  1. 创建一个新对象
  2. 将构造函数的作用域赋值给新对象
  3. 执行构造函数中的代码
  4. 返回新对象

person1 和 person2 分别保存着 Person 的不同实例,这两个对象都有一个 constructor (构造函数)属性,该属性指向 Person:

  console.log(person1.constructor == Person); //true
  console.log(person2.constructor == Person); //true
复制代码
构造函数的问题

使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍,例如 person1 和 person2 各自创建了一个 sayName() 方法,两个方法不是同一个 Function实例,以这种方式创建函数,会导致不同的作用域链和标识符解析:

function Person(name, age, job) {
 this.name = name;
 this.age = age;
 this.job = job;
 this.sayName = new Function("alert(this.name)");  // 与声明函数在逻辑上是等价的
}

console.log(person1.sayName == person2.sayName); // false
// 当然,我们可以通过把函数转移到构造函数外部并引用来解决这个问题
复制代码
对象

JavaScript 中万物皆对象,但是对象也分 object 和 function 等,例如:

let o1 = {}; 
function f1(){}; 
let a1 = []

console.log(typeof o1); //object 
console.log(typeof f1); //function 
console.log(typeof a1); //function 
复制代码

原型对象模式

那现在,我们想要通过更简单的方式让这些实例共享一些方法或者属性而不需要重新创建,有没有什么办法呢?

在 JavaScript 中,无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性是一个指针,指向函数的原型对象。

使用原型对象的好处就是可以让所有对象共享他所包含的属性或方法。

function Person() {}
Person.prototype.name = 'Nicholas';
Person.prototype.age  = 28;
Person.prototype.job  = 'Software Engineer';
Person.prototype.sayName = function() {
  alert(this.name);
}
  
var person1 = new Person();
person1.sayName(); // 'Nicholas'

var person2 = new Person();
person2.sayName(); // 'Nicholas'

console.log(person1.sayName == person2.sayName); //true
复制代码

默认情况下,所有原型对象都会自动获得一个 constructor(构造函数) 属性,通过这个构造函数,我们还可以继续为原型对象添加属性。同时,由于实例的 constructor 指向构造函数(实例为什么会有这个方法,后面讲),可以得出以下两个结论:

console.log(Person.prototype.constructor == Person); //true
console.log(person.constructor == Person); //true
复制代码
验证是否是该构造函数的实例

isPrototypeOf() 方法:

console.log(Person.prototype.isPrototypeOf(person1)); //true
console.log(Person.prototype.isPrototypeOf(person2)); //true
复制代码

Object.getPrototypeOf() 方法:这个方法返回原型的值

console.log(Object.getPrototypeOf(person1) == Person.prototype); //true 
console.log(Object.getPrototypeOf(person1).name); //"Nicholas"
复制代码

原型的共享属性使用

如果我们通过仅通过在实例中添加一个属性,且该属性与实例原型中的一个属性同名,那么就会在实例中创建该属性,该属性会将原型中的属性屏蔽。(可以理解为,实例调用一个属性时,会先去实例本身找,找不到就向下面的原型继续寻找,直到找到该属性或者返回 undefined )

function Person() {} 
Person.prototype.name = 'Nicholas'; 
Person.prototype.age = 28;

let person1 = new Person();
let person2 = new Person(); 

person1.name = "Greg"; 
console.log(person1.name); //"Greg"——来自实例 
console.log(person2.name); //"Nicholas"——来自原型
console.log(person1.__proto__.name); //"Nicholas"——来自原型
复制代码

可以使用delete删除实例中的属性,就可以恢复实例对原型中该属性的连接

delete person1.name
console.log(person1.name); //"Nicholas"——来自原型
复制代码
判断属性存在于原型或者实例的方法

hasOwnProperty() 方法:判断属性是否来自于实例

person1.name = "Greg"; 

console.log(person1.name); //"Greg"——来自实例 
console.log(person1.hasOwnProperty("name")); //true 

console.log(person2.name); //"Nicholas"——来自原型 
console.log(person2.hasOwnProperty("name")); //false
复制代码

通过 Person.prototype={} 设置等于以一个字面量行是创建新对象,会导致 constructor 属性不在指向 Person,本质上完全重写了默认的 prototype 属性,可以通过添加 constructor : Person 来重新建立关系。但这种发放会导致他的可枚举特性被更改为 true。

__proto__属性

当函数通过构造函数调用时,他所创建的对象中都会有一个隐藏的属性,该属性指向构造函数的的原型对象,我们可以通过__proto__来访问该属性。

console.log(person1.__proto__ == Person.prototype); //true
复制代码

原型链

原型链的基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法。原型链的核心是继承

想象一下,既然每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,那么,我们让原型对象等于另一个类型的实例会怎样?

显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。

function Father(){ 
    this.property = true; 
} 
Father.prototype.getFaValue = function(){ 
    return this.property; 
}; 
function Child(){ 
    this.chProperty = false; 
} 

//继承了 Father 
Child.prototype = new Father(); 
Child.prototype.getChValue = function (){
    return this.chProperty; 
}; 

let instance = new Child(); 
console.log(instance.getFaValue()); //true 
复制代码

构造器

我们可以这样创建对象

let obj = {}
let obj = new Object()
复制代码

可知,obj 是 Object 的一个实例,所以

obj.constructor === Object
obj.__proto__ === Object.prototype
复制代码

可知 Object 是一个函数,和上面的 Person 差不多。

同理,其他引用类型的数据,也有同样的结果

let b = new Array();
b.constructor === Array;
b.__proto__ === Array.prototype;

let c = new Date(); 
c.constructor === Date;
c.__proto__ === Date.prototype;

let d = new Function();
d.constructor === Function;
d.__proto__ === Function.prototype;
复制代码
console.log(typeOf Object) // function
console.log(typeOf Function) // function
console.log(typeOf Array) // function
console.log(typeOf Date) // function
console.log(typeOf Number) // function
console.log(typeOf String) // function
console.log(typeOf Boolean) // function
复制代码

所有的构造函数的实例的__proto__都指向构造函数的 prototype,所以又可得:

Number.__proto__ === Function.prototype  // true
Number.constructor == Function //true

Boolean.__proto__ === Function.prototype // true
Boolean.constructor == Function //true

String.__proto__ === Function.prototype  // true
String.constructor == Function //true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Object.__proto__ === Function.prototype  // true
Object.constructor == Function // true
Object.__proto__.__proto__ === Object.prototype // true

// 所有的构造器都来自于Function.prototype,甚至包括根构造器Object及Function自身
Function.__proto__ === Function.prototype // true
Function.constructor == Function //true

Array.__proto__ === Function.prototype   // true
Array.constructor == Function //true

RegExp.__proto__ === Function.prototype  // true
RegExp.constructor == Function //true

Error.__proto__ === Function.prototype   // true
Error.constructor == Function //true

Date.__proto__ === Function.prototype    // true
Date.constructor == Function //true
复制代码

那么Function.prototype的__proto__是谁呢?

console.log(Function.prototype.__proto__ === Object.prototype) // true
复制代码

函数本身也是对象,作为构造函数或者实例同时也继承了 Object.prototype 上的所有方法:toString、valueOf、hasOwnProperty等。

那么 Object.prototype.__proto__指向谁?

Object.prototype.__proto__ === null // true
复制代码

原型链的尽头,为 null。

现在我们再来看下原型链的经典图,是不是就看懂了呢

原型链图.jpg

本文借鉴了:

1.最详尽的 JS 原型与原型链终极详解
作者:Yi罐可乐

2.《JavaScript 高级程序设计》中文译本 第三版

全文,完!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享