JavaScript 原型链

524 阅读7分钟

前言

最近看了几篇关于原型链的文章,有所收获,但是总是很容易把自己绕蒙了,于是按照自己的理解重新整理了一下

原型链

概述

  1. 所有的类都是函数数据类型的(包括内置类),于是具有一下性质 ---- 稍后解释

    • 所有的函数都天生自带一个属性:prototype原型

    • prototype的属性值默认是一个对象数据类型值【堆】

    • 在对象prototype中存储的是供实例调用的公共属性和方法

    • 并且在类的prototype原型上,默认有一个属性constructor构造函数,属性值是当前类本身

说明:

    Array 是一个类
    typeof Array  // Function
    
    Array 中含有 prototype 属性
    prototype 中存储的是 供实例调用① 的公有属性和操作数组的方法(push、pop、splice)
    
    Array.prototype 存在属性constructor,值为: ƒ Array() { [native code] }
    
    ①:在创建数组时通过 new 创建的,所以我们使用的数组是Array的一个实例,也就是下面的arr,则arr.push('2')... 这就是上面所说的供实例调用。
    	let arr = new Array([1,2,3]);
         
  1. 所有对象数据类型值也天生自带的一个属性:__ proto__ 原型链

    • 对象数据类型值:

      普通对象、数组对象、正则、日期对象、类数组对象、DOM对象... 大部分实例(除基本数据类型外)也是对象类型

      「 万物皆对象,所有的值都是对象类型的 」

    • __proto__ 属性的属性值:表示当前实例所属类的原型prototype

      实例.__proto__ === 类.prototype

说明:

  let arr = new Array([1,2,3]);
  
  arr 是 Array 的实例,也是一个对象,因此arr具有__ proto__属性
  arr.__ proto__ === Array.prototype && 在前面说过prototype是对象,所以arr.__ proto__也是对象,所以__ proto__也含有__ proto__属性...

举例说明

// a
function Fn(){
   this.x = 100;
   this.y = 200;
   this.getX = function () {
   	console.log(this.x)
   }
}

// b
Fn.prototype.getX = function () {
   console.log(this.x)
}

// c
Fn.prototype.getY = function () {
    console.log(this.Y)
}

// d
let f1 = new Fn;
let f2 = new Fn;

// e
f1.getX();
f2.getY();
f1.__proto__.getX();
Fn.prototype.getY();

接下来,我会按照代码顺序进行说明,可能会细致一下,请耐心看完~

a. 1. 首先函数Fn会创建一个堆内存,内部存储代码字符串,以及包含prototype原型

a. 2. prototype原型对象中存储 constructor属性,属性值为 Fn

b.1. 在Fn的原型上添加属性getX

c.1. 在Fn的原型上添加属性getY

d.1. 创建实例f1,由实例.__proto__ === 类.portotype得 (如下图)

d.2. 创建实例f2,由实例.__proto__ === 类.portotype得 (如下图)

上图中Fn.prototype原型对象中__proto__没有指向,因此我们可以这样理解, ② 在不知道new谁出来的对象下多数的对象类型值都是Object内置类的一个实例,所以实例的__proto__指向Object的原型prototype

解释一下为什么最后Object.prototype.__proto__ 的值为null,这是因为我们上面说的 ②,并且Object 是所有对象数据类型的'基类',所以此处应该指向自己本身,这样毫无意义,所有最后的值为null

e.1. f1.getX()执行,在执行之前我们先明确一下实例.方法的执行顺序:

  • 首先看是否为自己的私有属性,如果是自己私有的,那么操作的就是私有的

  • 如果不是自己私有的,默认基于__proto__找到所属类原型上的公共属性和方法

  • 如果原型上也没有,则继续基于原型.__proto__向上查找,直到找到Object.prototype为止

  • 我们把这种查找机制称为'原型链查找机制'

实例.__proto__.方法 或者 类.prototype.方法 的执行顺序如下:

  • 跳过私有属性的查找,直接找所属类原型上公共的属性方法

  • 注意:__proto__IE浏览器中被保护起来了,不允许我们访问操作

e.2. f1.getX()执行

  • f1.getX 基于原型链查找机制,找到这个方法(私有方法)

  • 把找到的方法执行,确定方法中的this[点前面是谁this就是谁]

  • 代码执行算出答案皆可

  • 私有getX执行,方法中的thisf1, => console.log(this.x) => f1.x => 100

e.3. f2.getY()执行

  • f2.getY基于原型链查找机制,找到公共属性getY执行,console.log(this.Y) => f2.Y => 200

e.4. f1.__proto__.getX()执行

  • 执行公共的getXthisf1.__proto__,而在f1.__proto__上找不到x,所以f1.__proto__.xundefined

e.5 Fn.prototype.getY()执行

  • 执行公共的getYthisFn.prototype,在Fn.prototype上找不到y,所以Fn.prototype.yundefined

原型重定向

首先看下一段代码,分析一下:

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

a. 首先创建一个函数Fun,然后在Fun的原型上进行了重新的复制操作,所以此时Fun.prototype便指向了新创建的对象,这一步重新操作就是原型重定向

灵魂拷问

1.为什么要重定向 ?

答:为了方便批量给原型上扩充属性和方法

说明:我们通常在给原型上扩展方法时通常是这样写的③,不过当想批量的添加属性时代码就过于'累赘',所以这就是为什么要有原型重定向④

// ③

Fun.prototype.d = function () {}
Fun.prototype.e = function () {}
Fun.prototype.f = function () {}
...

// ④

Fun.prototype = {
    d: function () {},
    e: function () {},
    f: function () {},
    ...
}
  1. 原型重定向带来的问题:
  • 新定向的原型对象上没有constructor属性,结构不完整

  • 浏览器默认生成的原型对象因为缺少引用会被释放,可能导致原始加入的属性和方法丢失掉

  • 注意:内置类的原型使不允许重定向的,可能会导致内置的方法全部消失

b. 1. 创建实例var my_fun = new Fun()

b.2. 执行my_fun.b()

my_fun.b()按照原型链查找机制,b是私有的,thismy_funalert(this.a)字符串0

my_fun.c()按照原型链查找机制,在实例my_fun中并没有c,继续按照my_fun.__proto__向上查找,找到c并执行,this.a = 30thismy_fun,所以此时实例中a 变为30alert(this.a)值为字符串30

重写new

首先我们来先了解一在new的过程中都做了什么

function Dog(name) {
    this.name = name;
}
let dog = new Dog('aa')

通过打印实例对象dog我们可以总结出一下几点:

  • 首先创建一个实例对象,将 实例对象.__proto__ === 类.prototype

  • 在实例对象中存在name属性,说明将类Dog按照普通函数执行(私有上下文、作用域链、形参赋值、变量提升、代码执行...)

  • 类中的this指向实例对象

  • 最后看方法的返回结果,如果没有return或者返回基本数据类型,则返回实例对象,如果不是,则已返回值为主

按照下面代码执行结果来重写new

function Dog(name) {
    this.name = name;
}
Dog.prototype.bark = function () {
    console.log('wangwang');
};
Dog.prototype.sayName = function () {
    console.log('my name is' + this.name);
};
let sanmao = new Dog('三毛');
sanmao.sayName();   // my name is三毛
sanmao.bark();      // wangwang

说明:首先由一个Dog类,在其prototype上添加了barksayName属性

// func -> 类
// ...params -> 基于剩余运算符 传递给类的实参
function _new(func, ...params){  
    // 创建一个实例对象
    let obj = {};
    // 实例对象.__proto__ = 类.prototype
    obj.__proto__ = func.prototype;
    // 改变this指向
    let result = func.call(obj, ...params);
    // 根据返回结果类型 决定返回结果
    if(result !== null && (typeof result==='object' || typeof result==='function') ){
    	return result
    }
    return obj
}
let sanmao = _new(Dog, '三毛');
console.log(sanmao, 'sanmao');
sanmao.sayName();
sanmao.bark();

从上面的截图可以看出,我们已经实现了new的功能,但是在IE浏览器中对于__proto__不兼容,所以接下来进一步优化解决这个问题

let obj = {
   name: 'haha'
}
console.log(Object.create(obj))

Object.create([对象A]):创建一个空对象,并且将对象A作为它的原型 {}.__ptoro__ === 对象A

Object.create(): 报错 Object prototype may only be an Object or null

Object.create(null): 创建一个空对象,但是此对象的__proto__等于null(没有),此对象不再是任何类的实例

此时的new重写为

// func -> 类
// ...params -> 基于剩余运算符 传递给类的实参
function _new(func, ...params){  
   // 创建一个实例对象
   // 实例对象.__proto__ = 类.prototype
   let obj = Object.create(func.prototype);
   // 改变this指向
   let result = func.call(obj, ...params);
   // 根据返回结果类型 决定返回结果
   if(result !== null && (typeof result==='object' || typeof result==='function') ){
   	return result
   }
   return obj
}
let sanmao = _new(Dog, '三毛');
console.log(sanmao, 'sanmao');
sanmao.sayName();
sanmao.bark();

既然已经看到了这里我么在深入一下,new重写已经实现了,在其中我们使用了Object.create(),我们也来实现一下:

Object.create()是ES2015提供的方法,不兼容低版本浏览器

Object.create = function create(obj){
	if(obj === null || typeof obj !== 'object'){
    throw new TypeError('Object prototype may only be an Object')
    }
    function Anonymous() {};
    Anonymous.prototype = obj;
    return new Anonymous;
}

总结

篇幅略长,不知道看到这里有没有蒙了🤔 对于原型链我个人的理解是首先了解其原型链查找机制,然后是原型重定向,最后要了解在new过程中都做了什么,为了我们来更好的理解原型链。

如果有什么错误的地方希望不吝啬指出,及时改正✊

最后

biu biu biu ~ ❤️❤️❤️❤️ 点关注 🙈🙈 不迷路 点个赞哦😘