原型和原型链及给原型上扩展属性和方法
“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第1篇文章,点击查看活动详情”
一、原型和原型链【理解下面两句话】
思考:
var ary=[1,2,3];
为什么 ary就可以用Array这个类上面的公用方法:push、splice、pop...,甚至还可以用Object这个类上面的方法,比如toString呢?要想知道这个答案
咱们就需要来了解下原型「显示原型」 prototype 原型链「隐式原型」 proto
学会以下两句就会了👇
第一句
大部分“函数数据类型”的值都具备“prototype(原型/显式原型)”属性,
prototype 属性值本身是一个对象「浏览器会默认为其开辟一个堆内存,用来存储实例可调用的公共的属性和方法」
在浏览器默认开辟的这个堆内存中「原型对象」
有一个默认的属性“constructor(构造函数/构造器)”,属性值是当前函数/类本身!
函数数据类型
-
普通函数(实名或者匿名函数)
-
箭头函数[不具备prototype]
-
构造函数/类「内置类/自定义类」
- 创建一个函数,执行的时候用“new”来执行,这样这个函数就是类(构造函数)
-
生成器函数 Generator
- funciton *fn(){} 生成器函数
function fn(){ //普通函数
....
}
const fn=function fn(){} //匿名函数
....
不具备prototype的函数
- 箭头函数
- 基于ES6给对象某个成员赋值函数值的快捷操作👇
let obj = {
fn1: function () {}, //具备 prototype
// 和上面类似,但是函数不具备prototype
fn2() {}
};
console.dir(obj.fn);
console.dir(obj.fn2)
第二句
每一个“对象数据类型”的值都具备一个属性“proto(原型链/隐式原型)”,属性值指向“自己所属类的原型prototype”
对象数据类型值
- 普通对象{}
- 特殊对象:数组、正则、日期、Math、Error…
- 函数对象 function Fn(){}
- 实例对象 let f1=new Array(); //f1实例对象
- 构造函数.prototype Fn.prototype new Fn () 此时的Fn是构造函数【类】
- ...
二、【原型链查找机制】
-
访问对象的某个成员,首先看是否为自己私有的,如果是私有的,则直接使用;
-
如果不是私有的,默认基于 proto_ 找到所属类的prototype, 如果找到了,则使用类赋予其的这个公有的属性方法
-
如果还没有找到,则基于 类.protoType.proto 继续往上找 直到找到Object【基类].prototype为止
- 如果都没有,就是操作方法或者属性不存在
- 基于这种查找机制,帮助我们实现了实例既有私有的属性和方法,也有公有的属性和方法了
prototype原型对象上写的属性方法,都是供其调用的公共属性方法
Object是所有对象类型的“基类",所以在Object.prototype._proto_如果要指向也是指向自己,没有啥意义,所以其值是null
画图简单理解 原型和原型链
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;
在控制台输出
详细深度理解 原型和原型链
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); //false 两个都是私有的堆
console.log(f1.getY === f2.getY);//true =>根据__proto__查找到的是都是 fn.prototype
console.log(f1.__proto__.getY === Fn.prototype.getY);//true=》f1.__proto__.getY跳过查找私有的 直接查找的是公有的 Fn.prototype.getYh获取原型上的getY 指向的是同一地址
console.log(f1.__proto__.getX === f2.getX);//false=》@1调取的是公有的 @2调取的是私有的 所以false
console.log(f1.getX === Fn.prototype.getX);//false=>@1调取的是私有的 @2调取的是公有的 所以false
console.log(f1.constructor);//Fn函数堆
console.log(Fn.prototype.__proto__.constructor);//Object原型对象
// @1 先确定执行的是私有的还是实例公有的方法
// @2 确定方法执行中this “点原则”
// @3 代码执行中替换分析出来的this,找到对应的结果
f1.getX();
//this=f1
//console.log(this.x);//=>100
f1.__proto__.getX();
//this=f1.__proto__
// console.log(this.x);//=>undefined
f2.getY();
//this=f2
// console.log(this.y);=>200
Fn.prototype.getY();
//this=fn
//console.log(this.y);=>undefined
获取类(构造函数).prototype的属性的三个方法
let f1=new fn();
1.f1.getY();
2.f1.__proto__getY(); 跳过私有的查找
不推荐 IE不支持手动访问__ proto__ Fn.prototype.getY();
3.f1.getY();
这三种办法都可以访问到Fn.prototype上的getY方法,区别是方法执行,里面的this是不同的
私有和公用属性的检测
-
思路:
- 不论对象的私有属性中是否存在这个属性,只要它公有属性中有这个属性,【只要是实例所属类的原型上写的属性都是实例的公有属性,而且一直要到Object.prototype】,则结果就是true
-
具体实现:
- 首先获取当前实例this(obj)的原型对象@A,看@A中是否存在attr这个属性,
- 存在则直接返回true,说明attr是它的一个公有属性;
- 如果不存在,则找@A的原型对象...直到找到Object.prototype;
- 首先获取当前实例this(obj)的原型对象@A,看@A中是否存在attr这个属性,
-
如果整个过程中都没有找到对应的属性,则说明attr不是他的公有属性,返回false即可!!
-
Object.getPrototypeOf(实例):获取某个实例的原型对象
//把检测其公有属性的方法拓展到对象类的原型上【基类的原型上Object.prototype】
Object.prototype.hasPubProperty = function hasPubProperty(attr){
let proto = Object.getPrototypeOf(this);
while (proto) {
if (proto.hasOwnProperty(attr)) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
};
三、给类的原型上扩展属性或方法
(供其实例调取使用的公有属性和方法)
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
四、基于内置类原型扩展方法
内置类原型指向图:arr -> Array.prototype -> Object.prototype
let arr = [10,20,30];
//在内存中的存放👇
-
作为普通对象,存放的这些属性方法,是工具类方法,和它的实例没关系
- Array.from([val])//值转换为数组
- Array.isArray([Val]);//检测val值是否是数组
-
原型对象上编写的属性和方法,是为当前类所属的实例准备的;后期基于__proto__去查找调用的
画图理解
JS中有很多内置类,而且在内置类的原型上有很多内置的属性和方法,虽然内置类的原型上有很多的方法,但是不一定完全够项目开发所用,所以真实项目中,需要我们自己向内置类原型扩展方法,来实现更多的功能操作
1.Array.prototype.quick=function quick(){} (以数组为例)
优点:
- 使用起来方便 之前“quick(arr)” 现在“arr.quick()”
- 第二种方案,能保证quick中的this就是要操作的数组{格式肯定是数组},不需要像方案一 一样做格式校验
- 趋近于内置方法的处理
- 而且是扩展到了内置类的原型上,以后任何的上下文中基本上都可以直接调用,无需再把quick导入进来
缺点:
- 我们设置的方法名,不要和内置方法名一样{一般设置名字的时候,我们自己加一个前缀,例如:myXxx},如果一样,则我们写的方法会覆盖内置方法
浏览器为了保护内置类原型上的方法,不允许我们重新定向内置类原型的指向(严格模式下会报错)
数组快速排序 【方案1】
const quick=function quick(arr){
if(arr.length<=1) return arr;
//1. 取出数组中间项
let middleIndex=Math.round(arr.length/2),
middleValue=arr.splice(middleIndex,1)[0];
//2. 创建左右两个数组,用原始数组剩下的项比较,比中间项小的放在左边,大的放在右侧数组
let arrLeft=[],
arrRight=[];
for (let i = 0; i < arr.length; i++) {
let item=arr[i];
item>middleValue?arrRight.push(item):arrLeft.push(item);
}
return quick(arrLeft).concat(middleValue,quick(arrRight));
}
let arr = [12, 16, 8, 1, 24, 15];
console.log(quick(arr));
方案2 数组快速排序 【方案2】 基于向内置类原型扩展方法
Array.prototype.quick = function quick() {
// this -> arr 用this代替之前的传参 「特殊:不能这样执行 Array.prototype.quick()」
if (this.length <= 1) return this;
let middleIndex = Math.round(this.length / 2),
middleValue = this.splice(middleIndex, 1)[0],
arrLeft = [],
arrRight = [];
for (let i = 0; i < this.length; i++) {
let item = this[i];
item > middleValue ? arrRight.push(item) : arrLeft.push(item);
}
return arrLeft.quick().concat(middleValue, arrRight.quick());
};
let arr = [12, 16, 8, 1, 24, 15];
arr.quick();
2、链式写法:执行一个方法,返回的结果可以继续调用其所属类原型上的其它方法...
-
arr之所以能调用quick数组原型上的方法,是因为arr是Array的实例,
- 核心:需要把方法放在所属类的原型上
-
所以链式写法的实现思路很简单:只需要让上一个方法执行的返回结果依然是当前类的实例,这样就可以立即接着调用类原型上的其它方法了
arr.quick().reverse().map(item=>item*10).push('dongfangmiaomiao');
=>5
转载自:https://juejin.cn/post/7139132450172567583