原型与原型链

343 阅读13分钟

思维导图

原型

大部分“函数数据类型”的值都具备“prototype(原型/显式原型)”属性,属性值本身是一个对象。浏览器会默认为其开辟一个堆内存,用来存储当前类所属实例,可以调用的公共的属性和方法,在浏览器默认开辟的这个堆内存中「原型对象」有一个默认的属性“constructor(构造函数/构造器)”,属性值是当前函数/类本身。

原型链

属性值指向“自己所属类的原型prototype”每一个“对象数据类型”的值都具备一个属性“__proto __(原型链/隐式原型)”,对象数据类型值有: 普通对象 特殊对象:数组、正则、日期、Math、Error... 函数对象 实例对象 构造函数.prototype ...

  • object所属类的原型就是自己,自己不用指向自己,__proto __=null

构造函数

构造函数,是一种特殊的方法。主要用来在创建对象时初始化对象,即为对象成员变量赋初始值,总与new运算符一起使用在创建对象的语句中。

Array数组内置类

Array数组有prototype原型对象,原型对象提供的方法就是数组可以调用的方法,比较特殊的是在原型prototype的length属性为0。如果是new的对象 对象就是new的实例,如果不是new的对象,对象都是object的实例__proto__指向 原型。

函数数据类型

  • 普通函数(实名或者匿名函数)
  • 箭头函数
  • 构造函数/类「内置类/自定义类」
  • 生成器函数 Generator
  • ...

不具备prototype的函数

//箭头函数
const fn=()=>{}
//基于ES6给对象某个成员赋值函数值的快捷操作
      let obj = {
         fn1: function () {
             // 常规写法  具备prototype属性
          },
         fn2() {
            // 快捷写法  不具备prototype属性
         }
      };
       class Fn {
          fn() {} //这样的也不具备prototype属性
       };

每一个“对象数据类型”的值都具备一个属性“proto(原型链/隐式原型)”,

🌟举例

声明一个数组arr=[10,20,30];,浏览器默认开辟一个堆内存,arr的__proto__指向Array.prototype原型对象,所有对象都是Object的实例,Array.prototype的__proto__指向Object.prototype。(由于Object类所有对象的基类,所以Object.prototype.__proto__如果要指向也是执行自己,所以赋值为null)。

找数组forEach方法并执行

三者都是找Array.prototype上的forEach并执行,区别在于forEach方法中的this

  • arr
  • arr.__ proto___
  • Array.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;//new Fn();new Fn;带参数优先级20 不带参数new优先级19
let f2 = new Fn;
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);//0
console.log(f1.getX === Fn.prototype.getX);//私有 实例公有0
console.log(f1.constructor);//Fn
console.log(Fn.prototype.__proto__.constructor);//
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();

声明并定义函数Fn,有一个属性prototype,Fn.prototype中有一个构造器constructor属性值为Fn,私有属性x,y,getX。 Fn.prototype.getX = function (){console.log(this.x);}; 通过在原型上加方法,在原型上添加getX,getY方法(公有属性)。let f1 = new Fn;声明新的对象f1 f2,Fn不带参数(带参数优先级为20,不带参数优先级为19)console.log(f1.getX === f2.getX);原型上没有getX方法,不同的对象该方法不同,是对象私有的,false。console.log(f1.getY === f2.getY);getY是原型上的方法,原型上有方法先找原型上的方法,是f1 f2公有的,true。console.log(f1.__proto__.getY === Fn.prototype.getY);在f1,f2的原型链上找,f1和f2的原型链指向f1,f2基类的原型,getY是公有的。console.log(f1.__proto__.getX === f2.getX);f1.proto.getX公有的,f2.getX私有的。console.log(f1.getX === Fn.prototype.getX);f1.getX私有的,Fn.prototype.getX公有的。console.log(f1.constructor);输出function Fn()console.log(Fn.prototype.__proto__.constructor);输出function Object()。 f1.getX();this为f1,输出100。 f1.__proto__.getX();this为f1.__proto __,getX()要输出this.x,没有属性x,输出undefined。 f2.getY();this为f2,输出200。 Fn.prototype.getY();this为f2.__proto __,没有属性y,输出undefined。

总结
  • f1.getX() 私有的getX,this指f1,console.log(f1.x);输出100。
  • f2.getY() 原型上的getY,this指的f2,console.log(f2.y);输出200.
  • Fn.prototype.getY() 原型上的getY,this指的是Fn.prototype,console.log(Fn.prototype.y);查找在原型上的y的值为undefined。
  • f1. __proto __.getX() 在原型上找到getX(),this指的是f1. __proto __,没有x的值,输出undefined。

🌟举例

function C1(name) {
    if (name) {
        this.name = name;//如果传值,私有属性name
    }
}
function C2(name) {
    this.name = name;//不管传不传值,都有一个私有属性name
}

function C3(name) {
    this.name = name || 'join';//不传值为join,传值为name
}
C1.prototype.name = 'Tom';
C2.prototype.name = 'Tom';
C3.prototype.name = 'Tom';
alert((new C1().name) + (new C2().name) + (new C3().name));// "Tomundefinedjoin"

new C1().name没有传值,也就是没有私有属性name,往原型链上找Tom。new C2().name 私有属性值为undefined。new C3().name私有属性为join。

🌟举例

// 函数/构造函数
function Fn(name) {
    this.x = 100;//用this设置的属性,是函数的私有属性
    this.name = name;
}
// 原型「类/实例」在原型上加属性和方法 用原型上的方法必须用new创建类/函数的实例 不用new创建函数/类不走原型 只看作用域和上下文
Fn.prototype.y = 200;
Fn.prototype.getX = function () {};
Fn.prototype.getName = function () {};
Fn.s=9;
// 是一个对象「和前面没有直接的关系」
Fn.x = 1000;//Fn的实例对象Fn上加x属性 
n=new Fn;
Fn.getX = function () {};//在实例对象Fn上加私有方法
new Fn().getX();//new(构造函数)创建对象有原型和原型链 私有没有getX,往原型上找getX
Fn();//当作普通函数执行,函数没有return,返回undefined

输出Fn,原型上有属性y有方法getX和getName,私有属性:s为9,x为1000。n=new Fn;用new构造函数,输出n。n原型上有属性y,方法:getX,getName,有私有属性:x为100,name为undefined,z为20。

🌟举例 ES6里构造函数体&原型/静态加方法属性

基于class声明的构造函数必须基于new执行,不允许当作普通函数。基于function声明的构造函数基于/不基于new都能执行。

  • 将function函数改造,使function也不能基于new执行 基于new执行和不基于new执行,最关键的不同this指向不同,Sum();this指向window(严格模式下undefined) new Sum();this指向Sum的实例
function Sum() {
    if (!(this instanceof Sum))//如果this不是Sum的实例
      throw new TypeError('constructor Sum cannot be invoked without new');
    console.log('OK');
}
// Sum();
new Sum();
  • ES6中构造函数体
class Fn{
constructor(name) {
        // this -> 创造的实例对象 「this.xxx=xxx设置的私有属性」
        // this.x = 100;
        this.name = name;
    }
    x = 100; //等价于构造函数体中的“this.x=100” 如果私有属性值是固定值,不需要传参数,就可以这么写
 }

对比ES5中构造函数体

function Fn(name) {
    this.x = 100;
    this.name = name;
}
  • ES6设置原型上的属性和方法 属性:不能在类中直接写y = 200;,直接写设置的是实例的私有属性
Fn.prototype.y = 200;

方法:在类中直接加方法,就是设置在原型上,但是这样这样设置的函数是没有prototype的,类似于:obj={fn(){}}

class Fn(){
getX=function(){}//私有的
 getX() {}//原型上
    getName() {}
    }

getX(),getName()没有prototype属性

  • 设置静态私有属性和方法 在类中如下操作,通过类的实例能访问到
// 看做普通对象,设置私有属性「静态私有属性和方法」
    static x = 1000;
    static getX() {}

手写new

Ctor -> constructor缩写 构造函数,params -> 后期给Ctor传递的所有的实参信息。 1.创建Ctor的一个实例对象 2.把构造函数当作普通函数执行,让方法中的this当作实例对象 让obj的__proto __指向基类的prototype原型。3.确认方法的执行的返回值,如果没有返回值或者返回值是原始值,默认返回实例对象即可。

 function _new(Ctor, ...params) {
   // 1.创建Ctor的一个实例对象 
   // 实例.__proto__===Ctor.prototype
   let obj = {};
   obj.__proto__ = Ctor.prototype;
   // 2.把构造函数当做普通函数执行「让方法中的THIS->实例对象」
   let result = Ctor.call(obj, ...params);
   // 3.确认方法执行的返回值「如果没有返回值或者返回的是原始值,我们让其默认返回实例对象即可...」
    //typeof null ->object Symbol和bigint不能new
   if (result !== null && /^(object|function)$/.test(typeof result)) return result;
   return obj;
}
  • 面试题重写Object.create() Object.create([obj]):创建一个空对象,并且让空对象.__proto__指向[obj] “把[obj]作为新实例对象的原型”,[obj]可以是一个对象或者是null,但是不能是其他的值,Object.create(null) 创建一个不具备__proto __属性的对象「不是任何类的实例」(自己写的时候 Object.create(null) 创建一个具备__proto__属性的对象object)
Object.create = function create(prototype) {
   if (prototype !== null && typeof prototype !== "object") throw new TypeError('Object prototype may only be an Object or null');
   var Proxy = function Proxy() {}
   Proxy.prototype = prototype;
   return new Proxy;
};

基于Object.create(),手写new

function _new(Ctor, ...params) {
   let obj,
       result,
       proto = Ctor.prototype,
       ct = typeof Ctor;
   // 校验规则
   if (Ctor === Symbol || Ctor === BigInt || ct !== 'function' || !proto)
       throw new TypeError(`${Ctor} is not a constuctor!`);
   obj = Object.create(Ctor.prototype);
   result = Ctor.call(obj, ...params);
   if (result !== null && /^(object|function)$/.test(typeof result)) return result;
   return obj;
}
let sanmao = _new(Dog, '三毛');
sanmao.bark(); //=>"wangwang"
sanmao.sayName(); //=>"my name is 三毛"
console.log(sanmao instanceof Dog); //=>true

向原型上扩充方法优化

  • 纯粹对象 创建的是object的实例 对象.proto===Object.prototype
  • 面向对象 只画堆内存 不画栈内存
function fun() {
    this.a = 0;
    this.b = function () {
        alert(this.a);
    }
}
  • 1.逐一向原型对象上扩充属性和方法
fun.prototype = {
    b: function () {
        this.a = 20;
        alert(this.a);
    }
}
fun.prototype = {
    c: function () {
        this.a = 30;
        alert(this.a)
    }
}

缺点:1.麻烦:fun.prototype操作起来有一丢丢的麻烦「可以起一个小名」。

let proto = fun.prototype;
proto.b = function () {};
proto.c = function () {}; 

2.不聚焦:向原型上扩充方法的代码可能会分散开,这样不利于维护。

// 重定向可以解决这些问题
fun.prototype = {
    b: function () {},
    c: function () {}
};

但是重定向有缺点:1.会把之前原型里的属性和方法覆盖掉。2.JS很多内置类原型上有很多内置属性和方法,为保护浏览器不允许我们重新定向内置类原型的指向(严格模式模式下会报错)。

Array.prototype={
   name:'xxx'
}
Array.prototype //不变

我们可以使用合并的方法来解决:Object.assign([obj1],[obj2],...): 两个或者多个对象进行浅合并「右侧替换左侧」

  • 注意:并没有返回全新的一个合并后的对象,返回的值依然是obj1这个堆,只是把obj2中的内容都合并到obj1中。
let obj1 = {
    x: 10,
    y: 20,
    getX: function () {}
};
let obj2 = {
    x: 100,
    getY: function () {}
};
console.log(Object.assign(obj1, obj2));//x: 100,
console.log(Object.assign({}, obj1, obj2));//不改变obj1 obj2 返回新对象

内置类原型添加方法

内置类的原型上,虽然提供很多供其实例调取的属性和方法,但是不一定能完全满足我们的需求,此时我们需要自己向内置类的原型上扩充方法。好处:使用起来方便「实例.方法」;可以实现链式写法「核心:函数执行的返回值如果是某个类的实例,则可以直接继续调用这个类原型上的其他方法」;弊端:自己写的方法容易覆盖内置的方法「所以起名字最好设置前缀,例如:myXxx」

  • 不能通过重定向的方法在内置类原型上添加方法,非严格模式下不报错但是修改后无用,严格模式下报错但可以逐个添加。 数组增加去重方法
Array.prototype.unique = function unique() {
    // this -> arr 一般都是当前要操作的实例「无需传递要处理的数组,因为this存储的值就是」
    return Array.from(new Set(this));
}; 
let arr = [1, 2, 3, 4, 2, 3, 4, 5, 6, 2, 3, 4, 5, 6, 7, 2, 2, 3, 4, 6, 7];
console.log(arr.unique()); 

🌟举例

function fun() {
    this.a = 0;
    this.b = function () {
        alert(this.a);
    }
}
fun.prototype = {
    b: function () {
        this.a = 20;
        alert(this.a);
    },
    c: function () {
        this.a = 30;
        alert(this.a)
    }
}
var my_fun = new fun();
my_fun.b();//0
my_fun.c();//30

变量提升:声明并定义函数fun,fun的作用域EC(G),声明my_fun。代码执行:给fun原型中加入b,c方法。给my_fun赋值,调用my_fun的方法。在my_fun的堆内存中有b方法并调用,输出0;无c方法,沿着原型链查找,原型链指向my_fun基类的原型,里面有c方法,输出30。

🌟举例

// 函数/构造函数
function Fn(name) {
    this.x = 100;
    this.name = name;
}
// 原型「类/实例」
Fn.prototype.y = 200;
Fn.prototype.getX = function () {};
Fn.prototype.getName = function () {};
// 对象「和前面没有直接的关系」私有的
Fn.x = 1000;
Fn.getX = function () {};

Fn.getX();//找Fn私有的getX方法
new Fn().getX();//new(构造函数)创建对象有原型和原型链 私有没有getX,往原型上找getX
Fn();//当作普通函数执行,函数没有return,返回undefined

🌟举例

function Foo() {
    getName = function () {
        console.log(1);
    };
    return this;
}
Foo.getName = function () {
    console.log(2);//对象Foo的私有方法
};
Foo.prototype.getName = function () {
    console.log(3);//在原型上加方法
};
var getName = function () {
    console.log(4);//全局变量
};
function getName() {
    console.log(5);//全局方法
}
Foo.getName();//2
getName();//4
Foo().getName();//1 Foo()执行,形成私有上下文里,私有属性getName()
getName();//
new Foo.getName();
new Foo().getName();
new new Foo().getName();

  • xxx.xxx成员访问 优先级20
  • new xxx() 优先级20
  • new xxx 优先级19
  • 先根据优先级执行,优先级相同,从左到右执行 在全局上下文中,全局变量为Foo和getName(由于函数getName变量提升加定义,所以在未执行前getName的堆地址为0x001),变量提升:function Foo(){...} var getName; function getName(){...} Foo().getName();执行Foo(),形成私有上下文,作用域链<EC(FOO),EC(G)>,代码执行:修改全局变量的getName值,输出4。getName()输出4。new Foo.getName();Foo是对象,不是函数执行,构造对象优先级19,所以先成员访问,再构造对象。输出2。new Foo().getName();从左到右执行:先构造函数执行,再成员访问。没有私有getName方法,往原型链上找,输出3。new new Foo().getName();先给构造函数,再成员访问,最后构造对象。输出3。

🌟举例

function Fn() {
    let a = 1;
    this.a = a;
}
Fn.prototype.say = function () {
    this.a = 2;
}
Fn.prototype = new Fn;
let f1 = new Fn;
Fn.prototype.b = function () {
    this.a = 3;
};
console.log(f1.a);
console.log(f1.prototype);
console.log(f1.b);
console.log(f1.hasOwnProperty('b'));
console.log('b' in f1);
console.log(f1.constructor == Fn); 

全局上下文中,变量提升Fn。Fn构造函数,创建函数:作用域EC(G),代码字符串"let a = 1;this.a = a;",键值对name:Fn,length:0,__proto __:Function.prototype,prototype:Fn.prototype。Fn.prototype(也是一个对象,是对象就有__proto __属性;所有对象都是Object的实例)的__proto __指向Object.prototype。在Fn的原型上加方法say,创建新的实例对象,Fn的原型指向新的实例对象。新的实例对象的__proto __指向Fn.prototype。在Fn的原型上加b方法,即在新创建的实例上加b方法。创建一个实例对象f1,__proto __指向基类Fn的原型即new Fn。

  • 总结: new Fn,f1基类是Fn;Fn基类是Function;Function基类是Object。
  • 解题: f1.a:在f1中可以找到私有属性值,输出1。f1.prototype:f1里没有原型,按原型链向上查找都没找到,输出undefined。 f1.b:按原型链向上找,输出函数b。 f1.hasOwnProperty('b'):b相当于f1来说是公有方法,输出false。 'b'in f1输出true。 f1.constructor ==Fn f1没有constructor,沿原型链一直找到Fn.prototype找到constructor值为Fn,输出true。

函数的多种角色

函数

  • 普通函数(上下文/作用域)
  • 构造函数(类/实例/原型/原型链) 普通对象(键值对)
function Fn() {
    this.x = 100;
}
Fn.prototype.getX = function getX() {
    console.log(this.x);
};
let f1 = new Fn;
let f2 = new Fn;

  • 每个对象类型的值都具有__proto __属性
  • 大部分“函数数据类型”的值都具备“prototype(原型/显式原型)”属性,属性值本身是一个对象
  • 所有的对象都是object的实例
  • 所有函数(普通函数/构造函数/···)都是Funtion内置构造函数的一个实例
  • Object.prototype的属性__proto __的属性值为null
  • 所有函数也是对象,也有原型链属性__proto __ 构造函数Fn有属性prototype,属性值是对象Fn.prototype。用new声明f1,f2。因为每个对象类型的值都具有__proto __属性,所以给对象f1,f2,Fn.prototype,Object.prototype设置__proto __。因为所有的对象都是object的实例,所以f1,f2指向所属类的原型Fn.prototype,Function.prototype和Fn.prototype指向所属类的原型Object.prototype。因为所有函数也是对象,也有原型链属性__proto __,Fn,Object有属性__proto __,所有函数(普通函数/构造函数/···)都是Funtion内置构造函数的一个实例,所以:Fn,Function内置构造函数和Object内置构造函数指向Funtion.prototype(3条红色箭头)
Function.prototype===Function.__proto__//true
Fn.call===Function.prototype.call
//Fn没有call方法默认沿原型链向上找//true
Object.apply===Function.prototype.apply//true

Function和Object这两个类谁大?

Function instanceof Object//true
Object instanceof Function//true
Object.__proto__===Function.prototype//true

所有对象 包含函数对象 都是object的实例,都可以调取object.prototype上的方法 万物皆对象 JS中所有的值「排除原始值」都是对象数据类型的「函数也是一种特殊的对象」,所以所有的值都是Object的实例 XXXx instanceof Object -> true 函数比较特殊「普通函数/箭头函数/构造函数「内置/自定义」/生成器函数」即是对象也是函数。作为函数来讲,都是 Function的一个实例。 function是函数的基类 object是所有对象的基类 函数既是函数也是对象 具体谁大?不同类型结果不同。

  • 特殊 Object和Fn的原型是一个对象,Function原型是匿名空函数(没有prototype,我们把它当作其他原型对象一样处理)
Object.prototype
//输出 Object { … }
Function.prototype
//输出 function ()