上篇的最后我们提到了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:10
,1:20
,length:2
都是arr1
实例的私有属性; arr1
和arr2
里的内容互不冲突(原因在单例模式中讲过);
每一个实例对象,自己堆内存中储存的属性都是私有属性
无论是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
push
是arr1
实例的公有方法;但是是Array.prototype
的私有属性
hasOwnProperty
是arr1
实例的“公有属性方法”
- 对象的私有属性:存在自己的堆中,无需基于
__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
等数组原型上的方法,是因为arr
是Array
的实例,- 所以链式写法的实现思路很简单:只需要让上一个方法执行的返回结果依然是当前类的实例,这样就可以立即接着调用类原型上的其它方法了
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
完全看前面是否有“点”来决定
还是上面的例题
关