likes
comments
collection

原型和原型链及给原型上扩展属性和方法

作者站长头像
站长
· 阅读数 13

原型和原型链及给原型上扩展属性和方法

“我报名参加金石计划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”

对象数据类型值

戳这里看js的数据类型的相关内容

  • 普通对象{}
  • 特殊对象:数组、正则、日期、Math、Error…
  • 函数对象 function Fn(){}
  • 实例对象 let f1=new Array(); //f1实例对象
  • 构造函数.prototype Fn.prototype new Fn () 此时的Fn是构造函数【类】
  • ...

二、【原型链查找机制】

  1. 访问对象的某个成员,首先看是否为自己私有的,如果是私有的,则直接使用;

  2. 如果不是私有的,默认基于 proto_ 找到所属类的prototype, 如果找到了,则使用类赋予其的这个公有的属性方法

  3. 如果还没有找到,则基于 类.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;
  • 如果整个过程中都没有找到对应的属性,则说明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