likes
comments
collection
share

JS原型链总结

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

JS原型链总结

2022 / 5 / 10 By Awayer

本篇文章参考自www.cnblogs.com/wangfupeng1…

原型链部分————

一、前言(重要!!)

在本篇文章开始前,我要说明一个问题,在学习任何事物前都要明确一件事,就是:为什么要存在这个东西?这个东西的存在可以解决什么问题的。当你明白了这个东西存在的意义时,你也就了解他了,而不是把这件事物当成必须去死记硬背的学。 因为原型这个东西你不去明白他的原理就去死磕那个很乱的图,即使你当时明白了,但是过不了一段时间就会忘掉。 所以,接下来我要先说明一下这个东西存在的意义。

不同语言之间的继承

如果我们学过其他面向对象的语言,这个事情就很好说了。

继承就好比父母有一套房子,他们生了很多孩子,他们的孩子和他们共用这个房子。假如没有继承,那么他们的孩子就得每个人自己去盖一间房子。

(1) c++

就拿c++来举例是这样的:

#include<iostream>
#include<string>
using namespace std;

class CPerson{
	private:
		string name;
		int age;
	public:
		void sayHi(){
			cout << "hello" << endl;
		}
};

class CTeacher : public CPerson{	// 公有继承CPerson
	private:
		string subject;
	public:
		void teach(){
			cout << "上课了" << endl;
		}
		CTeacher(){}
};

int main(){
	CTeacher t1;
	t1.sayHi();		// hello
}

上述c++代码创建了一个CPerson的基类,定义了私有数据成员name和age,定义共有sayHi方法。创建子类CPerson,共有继承CTeacher,给CTeacher定义私有成员subject,共有方法teach,同时定义构造函数。此时在main中创建CTeacher的实例对象t1,调用sayHi方法,输出hello。

通过共有继承的方式能让子类实例访问到父类上的方法。

(2) JS

我们知道,在ES6之前,js是没有类这个东西的,那这导致了什么问题呢?假如我们还想实现刚才的功能:

function CPerson(name,age){
    this.name = name;
    this.age = age;
    this.sayHi = function(){
        console.log("hello");
    }
}

function CTeacher(subject){
    this.subject = subject;
    this.teach = function(){
        console.log("上课");
    }
}

我们写了两个构造函数,一个是父类,一个是子类。写到这里突然"妈妈生的''了,怎样才能让CTeacher构造函数拥有父类的数据呢?****

这时候就轮到我们的"万恶之源"prototype出手了:

function CPerson(name,age){
    this.name = name;
    this.age = age;
    this.sayHi = function(){
        console.log("hello");
    }
}

function CTeacher(subject){
    this.subject = subject;
    this.teach = function(){
        console.log("上课");
    }
}

CTeacher.prototype = new CPerson("Awayer",19);		// 将CPerson的实例对象传给CTeacher的原型
let t1 = new CTeacher('English')
t1.sayHi();		// hello
console.log(t1.name);    // Awayer

我们加了一行代码解决了这个问题,那就是将CPerson的实例对象传给CTeacher的原型。因为函数都有一个叫做prototype(原型)的对象,我们将CPerson的实例对象传给这个原型,那么CTeacher就拥有了这些数据。

看不懂?问题不大,下面将会细说!

二、JS中的数据类型

在ES6之前是6种数据类型:Number , String , Null , Boolean , undefined , Object

ES6新增了Symbol 类型

我们可以通过typeof来查看一个数据的类型

function show(){
    console.log(typeof x);	// undefined
    console.log(typeof 114514);		// Number
    console.log(typeof NaN);	// Number
    console.log(typeof 'Awayer');	// String
    console.log(typeof true);	// boolean

    console.log(typeof function(){});	// function

    console.log(typeof [1,2,3]);	// object
    console.log(typeof []);		// object
    console.log(typeof {a:10 , b:20});    // object
    console.log(typeof null);    // object
    console.log(typeof new Object(10));   // object
}
show();

上述代码列出了JS的数据类型,其中(undefined,number,string,boolean)为简单的值类型,余下的(function,Array,null,new Object(10)都是对象,他们是引用类型

引用类型的类型判断要用instanceof,如下:

let a = function(){};
console.log(a instanceof Object);	// true
let b = [1,2,3];
console.log(b instanceof Object);	// true

可见,引用类型都是对象,那么为什么typeof function( ){ }输出却是function呢?

还有instanceof是干什么的呢?后面的章节会有讲解。

三、JS中的对象

上述说到,函数是一种对象(不懂先往下看),但是typeof function(){}输出却是function,他不像数组一样,typeof[1,2,3]为object,数组就像是对象的一个子集。但是函数却不一样,他和对象的关系不是包含和被包含,更像是一种鸡生蛋蛋生鸡的感觉,为什么呢?(如果没看懂的话问题不大,后面讲instanceof的时候会详解,现在你只需要记住函数是一种对象

我们,平日中创建一个JS对象都是怎么写的呢?大部分应该都是这样吧:

let obj = {
	a:114514,
	b:function(){
		console.log("Awayer")
	}
}

但是,这其实是一种语法糖,并不是完整的写法,完整的写法应该如下:

let obj = new Object();
obj.name = "Awayer";
obj.age = 114514;
let arr = new Array();
arr[0] = 1;
arr[1] = 1;
arr[2] = 4;

并且其中的Object和Array都是function

console.log(typeof Object);    // function
console.log(typeof Array);    // function

那么,这就说明了,对象都是由函数创建的

四、原型prototype

在(二) 中我们说到函数是一种对象,对象是属性和方法的集合。在JavaScript中,每个函数都有一个属性叫做prototype(原型),这个属性他是一个对象,是的,prototype是一个对象,他里面默认只有一个constructor属性,指向这个函数本身,如下图:

JS原型链总结

prototype既然是一个对象,肯定不会只有一个constructor属性,比如我们来看看Object里面的prototype对象的属性里就有很多属性:

JS原型链总结

并且,我们可以在prototype里添加自己的属性,如下:

function Fn () {};
Fn.prototype.name = 'Awayer';
Fn.prototype.sayHi = function(){
    console.log(" Hello! ");
}

这样,Fn的prototype的属性中就有了3个属性。那么,prototype这样设计有什么用呢?我们来看下面这段代码

function Fn () {};
Fn.prototype.name = 'Awayer';
Fn.prototype.sayHi = function(){
    console.log("Hello!");
}

let person = new Fn();
console.log(person.name);	// Awayer
person.sayHi();    // Hello!

Fn是一个函数,person对象是通过Fn函数new出来的,这样person就可以调用到Fn的prototype属性。那么,他是如何调用到Fn中的prototype呢?就要去了解下面一个神奇的东西,隐式原型__ proto __

五、隐式原型proto

在(三)说到,每个函数都有一个prototype属性,那么再加一句话就是:每个对象都有一个__ proto __ 属性,称为隐式原型。这个proto很隐蔽, 隐蔽到有的编辑器中都没有这个属性的智能提示。我们通过浏览器的控制台来看一下:

JS原型链总结

欸这不是巧了,和上文中prototype的属性完全一致!其实,本质上就是被Object函数创建出来的,所以obj1.__ proto __=== Object.prototype,obj1本质上是被Object创建出来的,关系如下图:

JS原型链总结

(*图解:Object是一个函数,函数即对象,拥有一个prototype属性,prototype对象中有一个constructor属性指向自己。然后obj1是被Object函数new出来的一个对象,这个对象的__ proto __ 指向Object的prototype(图中画错了,应该是__ proto __ 指向prototype))

那么问题来了,在上述问题中Object的prototype也是一个对象,既然是对象那么他肯定就有一个__ proto __属性,那么它指向哪里呢?看下面这张图:

JS原型链总结

我们发现Object函数的prototype对象的__ proto __ 指向null,但是这是一个特例!

那么这样一来,又出现了一个问题:函数也是一个对象,那么他是不是也有一个__ proto __属性?答案是当然有!

函数本质上是由 Function()函数创建的,传统的写法和完整的写法如下:

let fn1 = function () {
    console.log("传统写法");
}
//完整写法
let fn2 = new Function("x","y","return x+y");
console.log(fn2(2,4));    //6

这也证明了函数本质上是由 Function()函数创建的。

好了,上述说了这么多总结就一句话:对象的__ proto __ 指向创建该对象的函数的prototype。所以Object.proto === Function.prototype

下面我们来看一张图,里面讲述了3个函数之间的关系:

JS原型链总结

(*图解:由上到下是3个函数,分别为:自定义函数Foo、Object函数、Function函数。先由最顶层的自定义函数Foo来看,由于函数是一种对象,他有一个prototype属性,prototype里面有个constructor属性指向函数本身。再往下看,Object是一个函数,他有一个prototpye属性,并且上面我们说了函数也是对象,对象就有一个__ proto __ 属性,他指向创建该对象(Object函数)的函数的prototype,也就是Funtion函数的prototype。再往下看,Function函数也是对象,他的 __ proto __ 属性指向就是Function函数自己的prototype,至于为什么自己指向自己是因为:Function是一个函数,这个函数由Function函数自身创建出来,所以就指向了自己)

还有一个问题:Function函数的prototype是一个对象,它的__ proto __应该指向创建它的函数的prototype,也就是Object函数的prototype,如下图

JS原型链总结

六、instanceof

在(二)中我们说到了,对于值类型应该用typeof判断类型;但是typeof判断引用类型时返回值只有object/function,你不知道它到底是obj还是arr还是new Number。

   console.log(typeof [1,2,3]);	// object
   console.log(typeof {a:10 , b:20});    // object
   console.log(typeof new Object(10));   // object

这时候就需要instanceof了,如下:

function fn(){};
let f1 = new fn();
console.log(f1 instanceof fn);    // true
console.log(f1 instanceof Object);    // true

上述代码中f1对象由构造函数fn创建,但是为什么“f1 instanceof Object”返回值为true?别急,我们先来讨论一下instanceof的判断规则:

instanceof 的判断逻辑是,顺着左值的隐式原型__ proto __ 开始,一直往下找(只能走proto线),直到和右值的prototype汇合为止(prototype是从这边等着的),那么上述代码的判断过程就是这样的:

JS原型链总结

红线一直往下找,蓝线在那等着。

再来个例子,这段代码:

Function instanceof Object    // true
Object instanceof Function    // true

在图中找就是这样的:

JS原型链总结

第二行是这样的:

JS原型链总结

理解了吗?现在我们尝试理解以下instanceof的底层逻辑,上代码!:

Function.__proto__ === Object.prototype    // false

我们发现结果是false,所以Function的隐式原型和Object的原型指向的不是同一个对象,也就是说Function不是Object new出来的,如果看图来说(上图)就是红线只走了一次,停在了Function的原型,没有和Object的prototype汇合。

但是如果我们尝试:

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

却发现Object是Function new出来的。

所以,instanceof的工作机制的一个循环,我们自己手写一个instanceof函数来看看:

function instance_of(L, R) {
    var O = R.prototype; 	// 用变量O保存右值的原型
    L = L.__proto__;	// 给L赋初始值,为L的隐式原型
    while (true) {    // 死循环实现proto找不到往下找
        if (L === null)      // 如果找不到,返回false
             return false;   
        if (O === L) 	
             return true;   
        L = L.__proto__;  // 重要! 实现每次proto找不到时接着往后.__ proto __
    }
}

看到这里如果还不是太明白可以退回去多看几遍。来,我们来道题来检测一下你是不是真的懂了:

function fn(){};
let f1 = new fn();
console.log(f1 instanceof Function);    // ???

给你来张参考图:

JS原型链总结

结果是false!,拿图来讲是这样的:

JS原型链总结

好了,一直在用这张图,我们都没分析这张图的具体内容,接下来我们来一条一条线具体的分析一下:

不要嫌烦,老老实实看完!

*图解:只看最上层,有一个构造函数Foo,通过这个构造函数new出来两个实例对象f1、f2,由于他们俩是对象,所以他们身上肯定有一个属性叫做隐式原型__ proto __ ,这个隐式原型指向创建这个对象的构造函数的prototype,也就是Foo.prototype;接下来看第二层,有一个Object函数,通过它new出了两个对象o1和o2,他俩的隐式原型指向创建他俩的函数的prototype,也就是Object。然后Object的原型是对象,所以它的原型有隐式原型,指向null。然后Object是函数,所有函数都是由Function创建的,由于Function是一个函数,所以它由自己创建!是的,Function是由自己Function创建的!所以你可以看到图中有一个循环,就是Function的__ proto__ 指向Function的原型prototype。同理,Function的原型是一个对象,它的__ proto __ 指向Object的prototype。最后最外圈的一条线,Foo是构造函数,所以它由Function创建,由于函数也是对象所以Foo也有__ proto __属性,指向创建这个对象的函数的prototype,也就是Function的prototype。

好了,说到这里让我们回归正题,instanceof的本质是判断复杂类型,他就是来判断一个实例对象是不是一个构造函数创建出来的。

七、JS中的继承

众所周知,JS中没有类,但是他是怎么实现继承的呢?当然是原型链力!先来看看JS是如何通过原型链实现继承的:

function Fn(){}
let f1 = new Fn();
f1.a = 1;
Fn.prototype.a = 100;
Fn.prototype.b = 200;
console.log(f1.a);    // 1
console.log(f1.b);    // 200

我们可以发现,f1身上如果有这个属性(a)可以直接访问到,如果访问这个对象身上没有的属性(b),那他去会顺着__ proto __这条原型链去找,最终在Fn的prototype上找到了,这就是原型链

JS原型链总结

也就是这张图,那么我们在访问属性时如何区分这个属性到底是他身上自带的还是从原型链上找的呢?这时就要用到HasOwnProperty了就象是这样:

JS原型链总结

那么,问题来了,hasOwnProperty是从哪来的?f1身上没有,Foo身上也没有,猜一猜:

JS原型链总结

对象的原型链是沿着__proto__这条线走的,因此在查找f1.hasOwnProperty属性时,就会顺着原型链一直查找到Object.prototype。

由于所有的对象的原型链都会找到Object.prototype,因此所有的对象都会有Object.prototype的方法。这就是所谓的“继承”。

本篇到此,完结。

转载自:https://juejin.cn/post/7242519176853962809
评论
请登录