JS中的原型和原型链

3,424 阅读12分钟

上篇的最后我们提到了hasOwnProperty是用来检测某个属性是否为当前实例的私有属性的,我们还自己编写了hasPubProperty用来检测某个属性是否为当前实例的公有方法的;私有方法上文中已经介绍,就是实例本身私有的方法,存在当前实例中;那什么是公有方法,他们又在哪里呢?这就是我们今天要讲的原型和原型链。

我们以数组为例:

每一个数组都是Array这个内置数组类的实例;

let arr1 = [10, 20];
let arr2 = [30, 40];

console.log(arr1 instanceof Array); //=>true
console.log(arr1.hasOwnProperty('push')); //=>false
console.log(arr1.push === arr2.push); //=>true
arr1.push(100); //=>对象.属性  说明PUSH是ARR1的一个属性,而且是公有属性(其它数组中常用的方法也都是数组实例的公有属性)

push等一系列数组的方法在哪里呢?

我们先记住三句话:很重要、很重要、很重要,重要的事说三遍

  • 1、每一个函数都天生具备一个属性:prototype(原型),prototype的属性值是一个对象(浏览器默认会给其开辟一个堆内存)

    • =>“原型对象上所存储的属性和方法,就是供当前类实例所调用的公有的属性和方法”
  • 2、在类的prototype原型对象中,默认存在一个内置的属性:constructor(构造函数),属性值就是当前类(函数)本身,所以我们也把类称为构造函数

  • 3、每一个对象都天生具备一个属性:__proto__(原型链),属性值是当前实例(对象)所属类的prototype原型

思维导图

一、前言

还是以Array数组内置类为例

类(自定义类还是JS内置类)都是函数数据类型

我们可以打开控制台输出:

为了更好的理解,我们每步一图

1、既然都是引用数据类型,就都会开辟一个堆内存,堆内存中存储代码字符串;如图

因为Array是内置类,所以存储的为原生代码,浏览器为了保护原生代码,不让我们查看具体内容,所以输出为[native code],如果是我们自己创建的自定义类是可以看见的;

2、此时我们创建了两个数组

这里我们记住一句话

所有的实例都是对象数据类型值(除基本数据类型值的实例外)

let arr1 = [10, 20];
let arr2 = [30, 40];

数组arr1中的0:101:20length:2 都是arr1实例的私有属性; arr1arr2里的内容互不冲突(原因在单例模式中讲过);

每一个实例对象,自己堆内存中储存的属性都是私有属性

无论是arr1还是arr2都可以调用数组的push...等方法,但是这些方法私有里又都没有,那这些方法怎么出来的呢?

二、原型和原型链

这里我们就用到了上面的三句话:

1、每个函数上都有一个prototype属性;属性值是一个对象

每一个函数(包括👇)都天生具备一个属性:prototype(原型),prototype的属性值是一个对象(浏览器默认会给其开辟一个堆内存)

  • 普通函数
  • 类也是函数类型的值

原型对象上所存储的属性和方法,就是供当前类实例所调用的公有的属性和方法

2、在prototype原型对象中,默认存在一个constructor属性,属性值就是当前函数本身

在类的prototype原型对象中,默认存在一个内置的属性:constructor(构造函数),属性值就是当前类(函数)本身,所以我们也把类称为构造函数

如果我们在控制台输出dir(Array)找到prototype找到constructor结果是Array,这样一直找会无限循环...

3、每一个对象都有一个__proto__属性,属性值是当前实例所属类的prototype原型

每一个对象都天生具备一个属性:__proto__(原型链),属性值是当前实例(对象)所属类的prototype原型 这里的对象为泛指:包括

  • 对象数据类型值
    • 普通对象
    • 数组对象
    • 正则对象
    • ...
  • 实例也是对象类型值(除基本值外)
  • 类的prototype原型属性值也是对象
  • 函数也具备对象的特征(它有一重身份就是对象类型)
  • ...

现在我们输出:

  • 1、arr1.length 或者 arr1[0] ;
    • => 是获取当前实例的私有属性值;
  • 2、arr1.push() ;
    • 首先找自己的私有属性,私有属性有,调取的就是私有属性
    • 如果没有,默认基于__proto__原型链属性,找所属类prototype原型上的公共属性和方法
    • (这种查找机制就是原型链查找)
  • 3、arr1.push() === arr2.push();
    • 找到arr1.push 在找到 arr2.push
    • 都是原型上的同一个方法,指向的都是同一个空间地址,所以为true;

所以我们说原型上存放的是实例的公共属性和方法;

三、原型链查找

  • 4、arr1.proto.push
    • 直接找所属类原型上的 push 方法,类似于 Arrary.prototype.push 这样找
    • arr1.__proto__.push === arr2.push === Array.prototype.push

我们知道arr1 arr2 实例对象,他们所属的类是 Array ,所以 arr1.__proto__===Array.prototype肯定没有问题,

那我们Array.prototype这个对象是谁的实例呢?

  • 1、肯定不是Array的实例(他是Array的原型),数组才是Array的实例
  • 2、他本身是一个对象;

所有的对象数据类型值,都是内置类Object的一个实例

Object的原型也是一个对象,每个对象都有一个__proto__属性指向当前所属类的原型,而所有对象数据类型,都是Object的一个实例;

我们发现他最后指向了他自己;指向自己就失去了原型链查找的意义,所以我们规定Object.prototype.__proto__ === null

Object 是所有对象的基类,在他的原型上的__proto__属性,如果存在也是指向自己的原型,这样没有意义,所以他的__proto__属性值为null

ARR1(数组的实例对象)的整个原型链:

  • 先找私有的,私有的没有
  • 基于__proto__找到所属类的原型Array.prototype;如果还没有
  • 基于Array.prototype__proto__找到Object.prototype ; 如果还没有
  • 则就是没有了

这一系列就是我们的原型链查找;

原型链查找机制:基于实例的__proto__找所属类的prototype

  • => 实例的私有属性方法
  • => 实例的公共属性和方法

基于这种查找机制,帮助我们实现了实例既有私有的属性和方法,也有公有的属性和方法了

这也是整个面向对象的核心。

我们可以打开控制台,输出dir[10,20]):

通过控制台的输出结果,可以证明我们上面画的图是没有问题的

  • 当我们使用arr1.hasOwnProperty
    • 实际上是找到=> arr1.__proto__.__proto__ => Object.prototype
  • arr1.hasOwnProperty("push") => false
  • Array.prototype.hasOwnProperty("push") => true

pusharr1实例的公有方法;但是是Array.prototype的私有属性

hasOwnPropertyarr1实例的“公有属性方法”

  • 对象的私有属性:存在自己的堆中,无需基于__proto__查找就有的
  • 对象的公有属性:自己堆中没有,需要基于__proto__prototype上的

原型链的图到这里就已经全部画完了😄

JS中的所有值,最后基于__proto__原型链,都能找到Object.prototype原型,也就是都是对象类的实例,也就是都是对象,这就是“万物接对象”

四、一道例题

老规矩,我们在来一道题

function Fn() {
	this.x = 100;
	this.y = 200;
	this.getX = function () {
		console.log(this.x);
	}
}
Fn.prototype.getX = function () {
	console.log(this.x);
};
Fn.prototype.getY = function () {
	console.log(this.y);
};
let f1 = new Fn;
let f2 = new Fn;
console.log(f1.getX === f2.getX);
console.log(f1.getY === f2.getY);
console.log(f1.__proto__.getY === Fn.prototype.getY);
console.log(f1.__proto__.getX === f2.getX);
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.constructor);
console.log(Fn.prototype.__proto__.constructor);
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();

做原型类的题目,没有比画图更好的方式了

图画完了,我们根据图直接写答案就可以啦😄

console.log(f1.getX === f2.getX); //=> false
console.log(f1.getY === f2.getY); //=> true
console.log(f1.__proto__.getY === Fn.prototype.getY); //=> true
console.log(f1.__proto__.getX === f2.getX); //=> false
console.log(f1.getX === Fn.prototype.getX); //=> false
console.log(f1.constructor); //=> Fn  实例的构造函数一般指的就是它所属的类
console.log(Fn.prototype.__proto__.constructor); //=> Object
f1.getX() ;  
   执行的是私有的getX => function () {console.log(this.x);}
   方法中的this => f1
   代码执行  
   console.log(this.x);  => f1.x  => 100

f1.__proto__.getX() ;
   执行的是原型上公有的getX  => function () {console.log(this.x);};
   方法中的this => f1.__proto__
   代码执行
   console.log(this.x);  => f1.__proto__.x => undefined

f2.getY() ;
    执行的是原型上公有的getY => function () {console.log(this.y);};
    方法中的this =>  f2
    代码执行
    console.log(this.y)  => f2.y =>200

Fn.prototype.getY() ;
    执行的是原型上的getY =>  function () {console.log(this.y);};
    方法中的this =>  Fn.prototype
    代码执行
    console.log(this.y)  => Fn.prototype.y => undefined

五、给类的原型上扩展属性或方法

(供其实例调取使用的公有属性和方法)

1、Fn.prototype.xxx = xxx(常用)

  • 向默认开辟的堆内存中增加属性方法
  • 缺点:如果需要设置很多属性方法,操作起来比较的麻烦(小技巧,给Fn.prototype设置别名)
  • 这类方式的特点都是默认开辟的堆中扩展属性方法,默认开辟的堆内存中存在constructor这个属性
let prop = Fn.prototype;
    prop.A = 100;
    prop.B = 200;
    prop.C = 300;

2、Object.prototype.xxx = xxx(不常用)

  • 内置类原型上扩展方法

3、f1.__proto__.xxx = xxx(基本不用)

  • 这样也可以,因为基于实例的__proto__找到的就是所属类的原型,也相当于给原型上扩展属性方法
  • 缺点:只不过这种方式我们基本不用,因为IE浏览器中,为了防止原型链的恶意篡改,是禁止我们自己操作__proto__属性的(IE中不让用__proto__)

4、原型重定向Fn.prototype = {...}(常用)

  • 我们自己手动开辟一个堆内存赋给Fn.prototype
  • 缺点1:自己开辟的堆内存中是没有constructor这个属性的;所以真实项目中,为了保护结构的严谨性,我们需要自己手动设置constructor
  • 缺点2:如果在重定向之前,我们向默认开辟的原型堆内存中设置了一些属性方法,重定向后,之前设置的属性方法都丢失了(没用了)
  • 解决办法:利用合并对象Object.assign(原来对象,新对象)
    • 合并过程中有冲突的情况以新的为主,剩余的不冲突的都合并在一起
    • 返回一个合并后的新对象 关于重定向,我们在看一道题👇
function Fn(num) {
    this.x = this.y = num;
}
Fn.prototype = {
    x: 20,
    sum: function () {
        console.log(this.x + this.y);
    }
};
let f = new Fn(10);
console.log(f.sum === Fn.prototype.sum); //true
f.sum();//20
Fn.prototype.sum();//NaN
console.log(f.constructor);//Object

六、给内置类的原型上扩展属性和方法

JS中有很多内置类,而且在内置类的原型上有很多内置的属性和方法,虽然内置类的原型上有很多的方法,但是不一定完全够项目开发所用,所以真实项目中,需要我们自己向内置类原型扩展方法,来实现更多的功能操作

1、Array.prototype.xxx = xxx(以数组为例)

  • 缺点:这种方法存在风险,我们自己设置的属性名可能会把内置的属性给覆盖掉

所以一般我们自己在内置类原型上扩展的方法,设置的属性名做好加上前缀

浏览器为了保护内置类原型上的方法,不允许我们重新定向内置类原型的指向(严格模式下会报错)

2、练习实例

需求1:模拟内置的PUSH方法

  • 在类的原型上编写的方法,让方法执行,我们一般都这样操作:实例.方法(),所以方法中的THIS一般都是我们要操作的这个实例,我们基于THIS操作就是操作这个实例
  • 实现思路
/*
 * JS中有很多内置类,而且在内置类的原型上有很多内置的属性和方法
 *    Array.prototype:数组作为Array的实例,就可以调取原型上的公共属性方法,完成数组的相关操作 => arr.push():arr基于__proto__原型链的查找机制,找到Array.prototype上的push方法,然后把push方法执行,push方法执行
 *        + 方法中的THIS是要操作的arr这个数组实例
 *        + 作用是向arr(也就是this)的末尾追加新的值
 *        + 返回结果是新增后数组的长度
 *  
 * 向内置类原型扩展方法:
 *    Array.prototype.xxx = xxx
 *    =>这种方法存在风险:我们自己设置的属性名可能会把内置的属性给覆盖掉
 *    =>一般我们自己在内置类原型上扩展的方法,设置的属性名最好加上前缀
 * 
 *    Array.prototype={...}
 *    =>浏览器为了保护内置类原型上的方法,不允许我们重新定向内置类原型的指向(严格模式下会报错)
 */

Array.prototype.myPush = function () {
	console.log('自己的PUSH');
};
let arr = [10, 20];
arr.myPush(100); 
  • 实现代码:
Array.prototype.myPush = function myPush(value) {
	// this:要操作的数组arr实例
	this[this.length] = value;
	return this.length;
};
let arr = [10, 20];
console.log(arr.myPush(100), arr);

let arr2 = [];
console.log(arr2.myPush('小芝麻'), arr2);

需求2:数组的原型上有SORT实现数组排序的方法,但是没有实现数组去重的方法,我们接下来向内置类原型扩展方法:myUnique,以后arr.myUnique执行可以把数组去重

Array.prototype.myUnique = function myUnique() {
	// this:当前要操作的数组实例
	let obj = {};
	for (let i = 0; i < this.length; i++) {
		let item = this[i];
		if (typeof obj[item] !== "undefined") {
			this[i] = this[this.length - 1];
			this.length--;
			i--;
			continue;
		}
		obj[item] = item;
	}
	obj = null;

	//为了实现链式写法
	return this;
};

let arr = [12, 23, 13, 23, 12, 12, 2, 3, 1, 2, 3, 2, 1, 2, 3];
arr.myUnique().sort((a, b) => a - b);
console.log(arr);

3、链式写法:执行完上一个方法,紧接着调用下一个方法执行

  • arr之所以能调用myUnique或者sort等数组原型上的方法,是因为arrArray的实例,
  • 所以链式写法的实现思路很简单:只需要让上一个方法执行的返回结果依然是当前类的实例,这样就可以立即接着调用类原型上的其它方法了
arr.myUnique().sort((a, b) => a - b).map(item => {
	return '@@' + item;
}).push('小芝麻').shift();
//Uncaught TypeError: arr.myUnique(...).sort(...).map(...).push(...).shift is not a function  
//=> 因为push返回的是新增后数组的长度,是个数字,不再是数组了,就不能继续调用数组的方法了

七、关于构造函数中的相关this问题

基于new执行,构造函数-函数体中的this是当前类的一个实例

给实例扩展的私有或者公有方法,这些方法中的this完全看前面是否有“点”来决定

还是上面的例题