乐趣区

js内功修炼之九阳神功原型链

写在前面

如果说 JavaScript 是一本武学典籍,那么原型链就是九阳神功。在金庸的武侠小说里面,对九阳神功是这样描述的:
“ 练成「九阳神功」后,会易筋洗髓;生出氤氲紫气;内力自生速度奇快,无穷无尽,普通拳脚也能使出绝大攻击力;防御力无可匹敌,自动护体,反弹外力攻击,成就金刚不坏之躯;习者速度将受到极大加成;更是疗伤圣典,百病不生,诸毒不侵。至阳热气全力施展可将人焚为焦炭,专门克破所有寒性和阴毒内力。” 可见其功法强大。
那么,如何修炼好 js 中的九阳神功呢?真正的功法大成的技术是从底层上去理解,那种工程师和码农的区别就在于对底层的理解,当你写完一行代码,或者你遇见一个 bug, 解决的速度取决于你对底层的理解。什么是底层?我目前个人的理解是“在你写每一行代码的时候,它将如何在相应的虚拟机或者 V8 引擎中是如何运行的,更厉害的程序员甚至知道每条数据的操作是在堆里面还是在栈里面,都做到了然于胸,这是 JavaScript 的内功最高境界(反正我现在是做不到,我不知道你们能不能,哈哈哈)”。


一、Js 原型链的简单理解

** 理解原型链之前首先要了解 js 的基本类型和引用类型:
1、基本类型
基本类型有 Undefined、Null、Boolean、Number 和 String。这些类型在内存中分别占有固定大小的空间,他们的值保存在栈空间,
我们通过按值来访问的。
基本类型:简单的数据段,存放在栈内存中,占据固定大小的空间。
2、引用类型
引用类型,值大小不固定,栈内存中存放地址指向堆内存中的对象。是按引用访问的
存放在堆内存中的对象,变量实际保存的是一个指针,这个指针指向另一个位置。每个空间大小不一样,要根据情况开进行特定的分配。
当我们需要访问引用类型(如对象,数组,函数等)的值时,首先从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。**

js 的原型链说简单也简单,说难也难。

首先说明:函数(Function)才有 prototype 属性,对象(除了 Object)拥有_proto_.
原型链的顶层就是 Object.prototype, 而这个对象的是没有原型对象的。
可以在 Chrome 输入:

Object.__proto__

输出的是:

ƒ () { [native code] }

可以看到这个没有.prototype 属性。

二、prototype 和_proto_的区别

我们知道原型是一个对象,其他对象可以通过它实现属性继承。

var a = {};
console.log(a.prototype);  //undefined
console.log(a.__proto__);  //Object {}

var b = function(){}
console.log(b.prototype);  //b {}
console.log(b.__proto__);  //function() {}

/*1、字面量方式 */
var a = {};
console.log(a.__proto__);  //Object {}

console.log(a.__proto__ === a.constructor.prototype); //true

/*2、构造器方式 */
var A = function(){};
var a = new A();
console.log(a.__proto__); //A {}

console.log(a.__proto__ === a.constructor.prototype); //true

/*3、Object.create()方式 */
var a1 = {a:1}
var a2 = Object.create(a1);
console.log(a2.__proto__); //Object {a: 1}

console.log(a.__proto__ === a.constructor.prototype); //false(此处即为图 1 中的例外情况)

var A = function(){};
var a = new A();
console.log(a.__proto__); //A {}(即构造器 function A 的原型对象)console.log(a.__proto__.__proto__); //Object {}(即构造器 function Object 的原型对象)console.log(a.__proto__.__proto__.__proto__); //null

instanceof 究竟是运算什么的?

我曾经简单理解 instanceof 只是检测一个对象是否是另个对象 new 出来的实例(例如 var a = new Object(),a instanceof Object 返回 true),但实际 instanceof 的运算规则上比这个更复杂。

// 假设 instanceof 运算符左边是 L,右边是 R
L instanceof R 
//instanceof 运算时,通过判断 L 的原型链上是否存在 R.prototype
L.__proto__.__proto__ ..... === R.prototype?

// 如果存在返回 true 否则返回 false
注意:instanceof 运算时会递归查找 L 的原型链,即 L.__proto__.__proto__.__proto__.__proto__… 直到找到了或者找到顶层为止。

所以一句话理解 instanceof 的运算规则为:

instanceof 检测左侧的__proto__原型链上,是否存在右侧的 prototype 原型。

图解构造器 Function 和 Object 的关系

我们再配合代码来看一下就明白了:

//①构造器 Function 的构造器是它自身
Function.constructor=== Function;//true

//②构造器 Object 的构造器是 Function(由此可知所有构造器的 constructor 都指向 Function)Object.constructor === Function;//true



//③构造器 Function 的__proto__是一个特殊的匿名函数 function() {}
console.log(Function.__proto__);//function() {}

//④这个特殊的匿名函数的__proto__指向 Object 的 prototype 原型。Function.__proto__.__proto__ === Object.prototype//true

//⑤Object 的__proto__指向 Function 的 prototype,也就是上面③中所述的特殊匿名函数
Object.__proto__ === Function.prototype;//true
Function.prototype === Function.__proto__;//true

当构造器 Object 和 Function 遇到 instanceof

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

如果看完以上,你还觉得上面的关系看晕了的话,只需要记住下面两个最重要的关系,其他关系就可以推导出来了:

1、所有的构造器的 constructor 都指向 Function

2、Function 的 prototype 指向一个特殊匿名函数,而这个特殊匿名函数的__proto__指向 Object.prototype

function、Function、Object 和{}

我们知道,在 Js 中一切皆为对象(Object),但是 Js 中并没有类(class);Js 是基于原型(prototype-based)来实现的面向对象(OOP)的编程范式的,但并不是所有的对象都拥有 prototype 这一属性:

var a = {}; 
console.log(a.prototype);  //=> undefined
 
var b = function(){}; 
console.log(b.prototype);  //=> {}
 
var c = 'Hello'; 
console.log(c.prototype);  //=> undefined

prototype 是每个 function 定义时自带的属性,但是 Js 中 function 本身也是对象,我们先来看一下下面几个概念的差别:
function 是 Js 的一个关键词,用于定义函数类型的变量,有两种语法形式:

function f1(){console.log('This is function f1!');
}
typeof(f1);  //=> 'function'
 
var f2 = function(){console.log('This is function f2!');
}
typeof(f2);  //=> 'function'

如果用更加面向对象的方法来定义函数,可以用 Function:

var f3 = new Function("console.log('This is function f3!');"); 
f3();        //=> 'This is function f3!' 
typeof(f3);  //=> 'function'
 
typeof(Function); //=> 'function'

实际上 Function 就是一个用于构造函数类型变量的类,或者说是函数类型实例的构造函数(constructor);与之相似有的 Object 或 String、Number 等,都是 Js 内置类型实例的构造函数。比较特殊的是 Object,它用于生成对象类型,其简写形式为{}:

var o1 = new Object(); 
typeof(o1);      //=> 'object'
 
var o2 = {}; 
typeof(o2);     //=> 'object'
 
typeof(Object); //=> 'function'

prototype VS_proto_

prototype 和 length 是每一个函数类型自带的两个属性,而其它非函数类型并没有(开头的例子已经说明),这一点之所以比较容易被忽略或误解,是因为所有类型的构造函数本身也是函数,所以它们自带了 prototype 属性:

除了 prototype 之外,Js 中的所有对象(undefined、null 等特殊情况除外)都有一个内置的 [[Prototype]] 属性,指向它“父类”的 prototype,这个内置属性在 ECMA 标准中并没有给出明确的获取方式,但是许多 Js 的实现(如 Node、大部分浏览器等)都提供了一个__proto__属性来指代这一[[Prototype]],我们通过下面的例子来说明实例中的__proto__是如何指向构造函数的 prototype 的:

var Person = function(){}; 
Person.prototype.type = 'Person'; 
Person.prototype.maxAge = 100;
 
var p = new Person(); 
console.log(p.maxAge); 
p.name = 'rainy';
 
Person.prototype.constructor === Person;  //=> true 
p.__proto__ === Person.prototype;         //=> true 
console.log(p.prototype);                 //=> undefined

图示解释上面的代码:

Person 是一个函数类型的变量,因此自带了 prototype 属性,prototype 属性中的 constructor 又指向 Person 本身;通过 new 关键字生成的 Person 类的实例 p1,通过__proto__属性指向了 Person 的原型。这里的__proto__只是为了说明实例 p1 在内部实现的时候与父类之间存在的关联(指向父类的原型),在实际操作过程中实例可以直接通过. 获取父类原型中的属性,从而实现了继承的功能。

核心图解

var Obj = function(){}; 
var o = new Obj(); 
o.__proto__ === Obj.prototype;  //=> true 
o.__proto__.constructor === Obj; //=> true
 
Obj.__proto__ === Function.prototype; //=> true 
Obj.__proto__.constructor === Function; //=> true
 
Function.__proto__ === Function.prototype; //=> true 
Object.__proto__ === Object.prototype;     //=> false 
Object.__proto__ === Function.prototype;   //=> true
 
Function.__proto__.constructor === Function;//=> true 
Function.__proto__.__proto__;               //=> {} 
Function.__proto__.__proto__ === o.__proto__.__proto__; //=> true 
o.__proto__.__proto__.__proto__ === null;   //=> true


从上面的例子和图解可以看出,prototype 对象也有__proto__属性,向上追溯一直到 null

new 关键词的作用就是完成上图所示实例与父类原型之间关系的串接,并创建一个新的对象;instanceof 关键词的作用也可以从上图中看出,实际上就是判断__proto__(以及__proto__.__proto__…)所指向是否父类的原型:

var Obj = function(){}; 
var o = new Obj();
 
o instanceof Obj; //=> true 
o instanceof Object; //=> true 
o instanceof Function; //=> false
 
o.__proto__ === Obj.prototype; //=> true 
o.__proto__.__proto__ === Object.prototype; //=> true 
o.__proto__.__proto__ === Function;  //=> false

原型链的结构
1. 原型链继承就是利用就是修改原型链结构(增加、删除、修改节点中的成员), 从而让实例对象可以使用整个原型链中的所有成员(属性和方法)
2. 使用原型链继承必须满足属性搜索原则

属性搜索原则
1. 构造函数 对象原型链结构图

function Person (){}; var p = new Person();

2.{} 对象原型链结构图

3. 数组的原型链结构图

4.Object.prototype 对应的构造函数

总结:
从本质上理解: 对象和函数都是保存在堆当中的引用类型, 后面一系列的操作都是为了使用或者访问其属性, 那么无论是 prototype 还是_proto_都是函数或者 Object 自带的指针,允许外界的其他一些函数或者 Object 去使用自己的一些属性。

更多的文章请关注公众号:码客小栈,每天不定时的更新 web 好文

退出移动版