巧解 JS 原型链

2,062 阅读9分钟

前言:

本文章带有强烈的个人风格主义,我以自己的方式理解原型链,还请各位路过的大佬们,多多指点,有啥不懂直接提出来,大家多多交流。

构造函数:

什么是构造函数?

var afunc = function (name) {		// afunc就是构造函数!!!
  this.name = name
  
  this.get = function() {
    return this.name;
  };
}

var robotA = new afunc('html');

console.log( robotA.get() );					// "html"

var robotB = new afunc('css');

console.log( robotB.get() );					// "css"

var robotC = new afunc('javascript');

console.log( robotC.get() );					// "javascript"

构造函数像一个克隆机器,它能够源源不断地克隆相同的东西,机器人A、机器人B..... ,都有相同的 属性和方法 ;

判断一个对象的构造函数:

就像上面例子一样,我们怎么找到,一个实例对象(robotA,robotB, robotC)的构造函数呢???

var afunc = function (name) {		// afunc就是构造函数!!!
  this.name = name
  
  this.get = function() {
    return this.name;
  };
}

var robotA = new afunc('html');

console.log(robotA.constructor);	// 函数 afunc

办法很简单,通过 实例对象(robotA)的 constructor 属性,就可以找到此实例对象的构造函数啦!!!

那大家有没有考虑过一个问题,我们声明的 afunc构造函数 又是谁帮我们生成的呢???用上述办法试试:

var afunc = function (name) {		// afunc就是构造函数!!!
  this.name = name
  
  this.get = function() {
    return this.name;
  };
}

var robotA = new afunc('html');

console.log(afunc.constructor);		// function Function() {  }

很显然,我们声明的构造函数 afun 是由另一个构造函数 Function 生成的!!!

读到这里,想必你肯定很疑惑,那么 这个 Function 函数又是谁构造的呢 ???

我们先把这些疑惑先放放,我后面会解释,在这之前,我们先看看,JS 内置的像 Function 一样的函数有哪些?

JS 内置的构造函数:

聪明的读者,想必都已经想到了,我们声明一个数字、字符串等等内置类型都 可能 有构造函数!!!

// 7种内置类型依次试试,不知道哪7种的同学可以看看我另一篇文章 《JS灵巧判断7种类型的方式》

/*-----------------显然两个货没有构造函数---------------------*/
console.log(null)				   // null

console.log(undefined)			   // undefined

/*-----------------七种类型中的其他 5 种类型--------------------*/

console.log(Number);			   // ƒ Number() {  }

console.log(String);                // ƒ String() {  }

console.log(Object);			   // ƒ Object() {  }

console.log(Boolean);               // ƒ Boolean() {  }

console.log(Symbol);                // ƒ Symbol() {  }

--------------------7种类型之外的内置构造函数---------------------*/

console.log(Date);				   // ƒ Date() {  }

console.log(RegExp);			   // ƒ RegExp() {  }

/*--------------------注意:Math不是构造函数而是一个对象------------*/

console.log(Math);				   // [object Math] { ... }

大概就列出来这么些 内置函数 吧,那我们要怎样使用这些构造函数呢???

当我们声明一个字符串、数字时,这些函数就会开始工作,证明这个真理:

var n = 123;				       // 当你写的 123 会被浏览器用 Number() 内置函数构造出来;

console.log(n.constructor);			// ƒ Number() { }

var s = 'javascript';

console.log(s.constructor);			// f String() { }

// 懒,其他我就省略啦 ......

// 其实你也可以用 new 关键字声明一个 数字、字符......,不过用这种方法的人该多蠢!!!

var n = new Number(123);

var s = new String('javascript');

console.log(typeof n);			// "object"

console.log(typeof s);			// "obejct"

console.log(Number(n));			// w3c 上是通过强制转换来获取 123 这个值的,

你可以很清晰地看到,其实,我们的这里个 内置函数 跟我们声明的 afunc 没有区别,都是同样的操作!!!

现在,我们再来看一个有趣的现象,我们这些内置函数的构造函数又是谁???

Number.constructor	 // ƒ Function() {  }  构造者是 Function 函数

String.constructor	 // ƒ Function() {  }  构造者是 Function 函数

Object.constructor	 // ƒ Function() {  }  构造者是 Function 函数

Boolean.constructor	 // ƒ Function() {  }  构造者是 Function 函数

Symbol.constructor	 // ƒ Function() {  }  构造者是 Function 函数

Date.constructor	 // ƒ Function() {  }  构造者是 Function 函数

RegExp.constructor	 // ƒ Function() {  }  构造者是 Function 函数


Math.constructor	 // ƒ Object() {  }  注意:Math已经不是一个函数了,构造者是一个Object函数!!!

现在,我们清楚了:

  1. 我们每声明一个函数,浏览器解析到这个函数是会交给内置函数 Function 来构造!!!
  2. 再看看 Math 对象,你也已经可能猜到,对象是由 f Object() 函数所构造的!!!

相类似,我们用 var关键字声明一个字符串、数字、布尔值等等,全部都是由浏览器解析然后交给内置函数处理!

函数对象和普通对象

估计看到标题你就傻了吧,函数对象是什么鬼?其实,函数的本质也是一个对象,只不过函数的能力很强大罢了!

// 如何区分,函数对象和普通对象???

var o = {};    
    
var f = function(){ };

console.log( o.constructor );			// ƒ Object() { }

console.log( f.constructor );			// ƒ Function() { }

结论:普通对象的构造函数是 Object() , 函数对象的构造函数是 Function();

原型的本质:

本篇文章是讲原型,前面是基础知识,现在终于到正餐啦!!!

函数对象与普通对象的区别:

函数对象和普通对象是有区别的,那我们怎么去分辨这种区别呢???

var o = {};  			       // object(普通对象) 

console.log( o.prototype );     // undefined

console.log( typeof o );		// "obejct"


/*----------------------我是你的分割线-------------------------*/


var f = function(){};           // function(函数对象)

console.log( f.prototype );     // {......}

console.log( typof f );		   // "function"

console.log( typof f.prototype );   // "object" 原型是一个普通对象

结论:

1. 函数对象的类型是 function,普通对象的类型是 object!!!

2. 函数对象 有 原型 ( prototype ),普通对象 没有 原型 prototype 的!!!

每声明一个函数,就会有一个产生一个原型,这个原型的 引用 就保存在 函数对象的 func.prototype 上面;

为什么要用原型?

回答这个问题前,还需要一点前置知识 。。。。。。

理解什么是 引用:

引用的概念出自于 C++ ,引用 的实质是 指针,不过用 引用 来思考 javascript 中的对象我想更为合适;

首先,我们所声明的每一个变量都是会占用计算机资源的,它们被保存在计算机内存的某个位置;

引用也会占据空间,小到一个小小的数字,大到一个巨大的对象:

如图,所示,引用,就是 “外号” ,一个人可以有很多种外号,无论叫你哪一个外号,都是指你本身!!!

这是如何体现在代码里面呢?

小明,有两个外号,狗蛋 和 嘤嘤嘤;

小红,有两个外号,二狗 和 哈哈哈;

那么,小明是不是小红呢?小明和小红大家都很熟悉了,不是一个人;

var xiaoming = {name: '小明'};	// 我们用小明代指这个对象

var goudan = xiaoming;  // 小明 有一个外号叫做 狗蛋

var yingyingying = xiaoming;    // 小明 有一个外号叫做 嘤嘤嘤

console.log( goudan === xiaoming ); // true 狗蛋 是 小明

console.log( yingyingying === xiaoming );   // true 嘤嘤嘤 是 小明

/*------------我是分割线-----------------*/

var xiaohong = {name: '小红'};	// 我们用小红代指这个对象

var ergou = xiaohong;	  // 小红 有一个外号叫做 二狗

var hahaha = xiaohong;     // 小红 有一个外号叫做 哈哈哈

console.log( ergou === xiaohong );	 // true 二狗 是 小红

console.log( hahaha === xiaohong );	 // true 哈哈哈 是 小红

/*----------------我是分割线---------------------*/

console.log( xiaohong === xiaoming );	// false 小明 不是 小红 <= 这个例子虽然无聊但是有用

很显然的,我们用 var 声明的变量就叫做 引用(别名) 我们通过这个 引用 来找到这个对象,在计算机里面的位置。

初次认识属性 __proto__ :

在回答为 什么要用原型 这个问题时,我们还需要明白一个属性 __proto__

我这里先不阐述 __proto__ 属性具体的定义和概念,以免大家思维混乱 。。。。。。

var afunc = function (name) {		// afunc就是构造函数!!!
  this.name = name
  
  this.get = function() {
    return this.name;
  };
}

afunc.prototype = {			
  set: function(newName) {
    this.name = newName;
  }
}

var robotA = new afunc('html');

console.log( robotA.prototype );	 // undefined 前面文章已经解释了 普通对象没有 原型

console.log( robotA.__proto__ );	 // {set: function() {...}} 找到了原型。

robotA.set('css');

console.log(robotA.get());			// "css"

现在你能够理解,robotA.__proto__afunc.prototype 都仅仅是 afunc 的原型的 引用 了吗???

如果,不理解,再仔细想想,思考一下,在之后的内容里,我会展示 __proto__ 更为详细的讲解;

当我们调用 robotA.get() 方法时,JS 会先去 构造函数 里面找是否有此方法,没有的话就到 原型 里面找!!!

回答为什么要用原型:

我们明白了什么是引用,就非常好解释,为什么需要用原型。

var afunc = function (name) {		// afunc就是构造函数!!!
  this.name = name
  
  this.get = function() {
    return this.name;
  };
}

afunc.prototype = {		   // 调用原型对象
  set: function(newName) {
    this.name = newName;
  }
}

var robotA = new afunc('html');

var robotB = new afunc('css');

var robotC = new afunc('javascript');

robotA === robotB			// false 这两在计算机中的对象 是不一样

robotA === robotC			// false 

robotB === robotC			// false 

robotA.__proto__ === robotB.__proto__		// true 这两个引用所指的对象 是相同的

robotA.__proto__ === robotC.__proto__		// true

robotB.__proto__ === robotC.__proto__		// true

仔细想想,robotArobotBrobotC ,这是分别是三个对象的 引用(别名)

计算机为这三个对象,分配了,三个存储空间,我们通过引用,找到这个存储空间的位置,然后执行!!!

robotA.__proto__robotB.__proto__robotC.__proto__ 也是三个引用(别名)

但是,这上述三个引用指向的内容,都是 afunc 函数的 原型 ,计算机里只保存唯一的一个 afunc 函数的 原型 ;

这样的原型有什么好处???

如果,你需要 new 1000 个对象,这 1000 个对象就会分配 1000 个对象的存储空间;

试想,每次 new 相同的 属性和方法 ,这样系统的开销就会变得非常大!!!

我们把相同的属性和方法放在一起,抽象为一个原型,这 1000 个对象都访问原型上的 共有方法

这样岂不是,美滋滋!!!

谈谈原型链:

终于到这一步了,感觉手都写酸了。。。。。

原型链,肯定跟原型有关啦,那这个关系到底是什么?

理解 __proto__ 的本质:

每一个对象,不管是函数对象或者普通对象,都会有 __proto__ 属性。

但是,这个属性已经 不推荐使用 了,但是为了展示什么是 原型链,我用了它。

如果想了解原因:请狠狠地戳这里,了解ES6标准;

var o = { };

console.log( o.__proto__ );	// {......} 生成 o 对象的原型;

console.log( o.__proto__.constructor );	// 生成 o 对象的构造函数 f Object() { };

console.log( o.__proto__.__proto__ );	// null; 到头啦,最初的起点 null !!!

关于 {......} 代表的是 object 起点原型,它也是一个普通对象!!!;

仔细思考,这两段代码,我不在这里阐述过多的东西,其实这里很绕的;

现在,来 函数对象 啦,仔细观察,此过程,比单纯的 普通对象 要复杂!!!

var f = function() { };		

console.log( f.__proto__ );	// ƒ () {  } 生成 f 函数对象的原型

console.log( f.__proto__.constructor );	// 生成 f 函数对象的构造函数 f Function() { }

console.log( f.__proto__.__proto__ );	// {......} 生成 f 函数对象的原型的原型 

console.log( f.__proto__.__proto__.constructor );	// ƒ Object() { } 

console.log( f.__proto__.__proto__.__proto__ );	  // null 又到头啦!!!

如果,你理解了这段代码,也就理解了,为什么说,函数对象也是对象这句话了。

现在,我们来模拟一下 ,从 new 一个对象,开始的原型链过程 !!!

var f = function() { };		

f.prototype = {
  name: "javascript"
}

var obj = new f();

console.log(obj.__proto__);				// { name: "javascript" } 构造 obj 普通对象的 原型
 
console.log(obj.__proto__.__proto__);	  // { ...... } 构造 obj 普通对象原型的 原型

console.log(obj.__proto__.__proto__.__proto__);	  // null 到头啦!!!

/*------------这是你需要区分的东西----------------*/

console.log(f.prototype);				// f 函数对象的原型

console.log(obj.prototype);				// undefined 普通对象没有原型!!!

其余的内容留给读者自己去思考。试着想一想,能否画出整个内置函数对象的原型链关系图

重要参考和感谢:

感觉不错,点个赞再走呗!!!

在此对上面链接或者书籍的作者表示感谢和支持,欢迎大家提出问题!!!