JS中的原型和原型链

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

咱们以数组为例:java

每个数组都是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等一系列数组的方法在哪里呢?浏览器

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

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

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

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

思惟导图

1、前言

仍是以Array数组内置类为例spa

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

咱们能够打开控制台输出:

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

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

由于Array是内置类,因此存储的为原生代码,浏览器为了保护原生代码,不让咱们查看具体内容,因此输出为[native code],若是是咱们本身建立的自定义类是能够看见的;

二、此时咱们建立了两个数组

这里咱们记住一句话

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

let arr1 = [10, 20];
let arr2 = [30, 40];
复制代码

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

每个实例对象,本身堆内存中储存的属性都是私有属性

不管是arr1仍是arr2均可以调用数组的push...等方法,可是这些方法私有里又都没有,那这些方法怎么出来的呢?

2、原型和原型链

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

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

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

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

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

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

在类的prototype原型对象中,默认存在一个内置的属性:constructor(构造函数),属性值就是当前类(函数)自己,因此咱们也把类称为构造函数

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

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

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

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

如今咱们输出:

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

因此咱们说原型上存放的是实例的公共属性和方法;

3、原型链查找

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

咱们知道arr1 arr2 实例对象,他们所属的类是 Array ,因此 arr1.__proto__===Array.prototype确定没有问题,

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

  • 一、确定不是Array的实例(他是Array的原型),数组才是Array的实例
  • 二、他自己是一个对象;

全部的对象数据类型值,都是内置类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原型,也就是都是对象类的实例,也就是都是对象,这就是“万物接对象”

4、一道例题

老规矩,咱们在来一道题

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
复制代码

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

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

一、Fn.prototype.xxx = xxx(经常使用)

  • 向默认开辟的堆内存中增长属性方法
  • 缺点:若是须要设置不少属性方法,操做起来比较的麻烦(小技巧,给Fn.prototype设置别名)
  • 这类方式的特色都是默认开辟的堆中扩展属性方法,默认开辟的堆内存中存在constructor这个属性
let prop = Fn.prototype;
    prop.A = 100;
    prop.B = 200;
    prop.C = 300;
复制代码

二、Object.prototype.xxx = xxx(不经常使用)

  • 内置类原型上扩展方法

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

  • 这样也能够,由于基于实例的__proto__找到的就是所属类的原型,也至关于给原型上扩展属性方法
  • 缺点:只不过这种方式咱们基本不用,由于IE浏览器中,为了防止原型链的恶意篡改,是禁止咱们本身操做__proto__属性的(IE中不让用__proto__)

四、原型重定向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
复制代码

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

JS中有不少内置类,并且在内置类的原型上有不少内置的属性和方法,虽然内置类的原型上有不少的方法,可是不必定彻底够项目开发所用,因此真实项目中,须要咱们本身向内置类原型扩展方法,来实现更多的功能操做

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

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

因此通常咱们本身在内置类原型上扩展的方法,设置的属性名作好加上前缀

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

二、练习实例

需求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);
复制代码

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

  • 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返回的是新增后数组的长度,是个数字,再也不是数组了,就不能继续调用数组的方法了
复制代码

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

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

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

仍是上面的例题

相关文章
相关标签/搜索