js深入学习绕不开的原型知识

最近在看underscore源码,涉及到js原型相关的知识,于是重温了一遍,再次做下记录。

js原型是其语法的一个难点,也是一个重点,要深入学习js必须掌握的点。要想读懂别人的框架和库,了解这些基础知识是必不可少的。
js原型主要为了提取公共属性和方法,实现对象属性和方法的继承。说到原型,可能就有几个相关的词:prototype、__proto__、constructor、instanceof。下面通过这几个关键词来一一讲解原型。

说到原型,先说一个概念,js里函数(function)是一种特殊的对象,有个说法是js里一切都是对象,这个说法不正确,要排除一些特殊类型,如:undefined, null (虽然null的typeof, toString返回类型是object,历史遗留bug)。

js里所有的对象都有proto属性,可以称为隐式原型,这个属性在低版本ie浏览器中无法读取到。一个对象的隐式原型指向构造该对象的构造函数的原型,使得该对象能够继承其构造函数原型上的属性和方法。

函数对象除了具有proto这个属性外,还有一个专有属性prototype。这个属性指向一个对象(命名a对象),从该函数实例化的对象(命名b对象)。b对象能共享a对象的所有属性和方法。反过来a对象的constructor属性又指向这个函数。

constructor 属性是专门为 function 而设计的,它存在于每一个 function 对象的prototype 属性中。这个 constructor 保存了指向 function 的一个引用。

constructor和prototype被设计时是构造函数和原型间相互指向,可以看成互逆的,由于实例继承了prototype对象上的属性(包括constructor),故实例的constructor也是指向构造函数。虽然他们俩是互逆,但是两者没有必然联系,修改其中一个的指向,另一个并不会变。所以在js中通过原型来继承时,一般替换原型时,会附带替换掉constructor的指向。

// constructor与prototype
Object.prototype === Object.prototype
Object.prototype.constructor === Object

// 实例指向构造函数
var a = new Object({});
a.constructor === Object;

// 修改一个指向
function a() {}
a.prototype = {
  say: function () {
    console.log('hello world');
  }
}
a.prototype.constructor === Object //true
// 因为a.prototype重新赋值时,直接是赋值的一个对象,
// 这个对象没有通过构造函数来生成,默认就会以new Object方式。故构造函数就是Object。
// 所以一般要手动重新指向构造函数
a.prototype.constructor = a;

constructor设计初是被用来判断对象类型的,由于其易变性,一般不使用它来做判断,使用instanceof来替代它。

instanceof运算符,它用来判断一个构造函数的prototype属性所指向的对象是否存在另外一个要检测对象的原型链上。一般用来判断一个实例是否从一个构造函数实例化过来,用一个函数模拟instanceof函数:

function _instanceof(A, B) {
  var O = B.prototype;
  A = A.__proto__;
  while (true) {
    if (A === null) // 循环查找原型链,一直到Object.prototype.__proto__ = null
      return false; // 退出循环
    if (O === A)
      return true;
    A = A.__proto__;
  }
}

说了这么多是不是觉得有点绕,拿出我的杀手锏,祭出我收藏的一张图,该图很清晰的解释了这些关系。看了这张图后,瞬间理清了原型,废话不多说,上图:

js深入学习绕不开的原型知识_第1张图片
js原型

看了上面的图后相信整个原型比较清晰了,下面说说整个原型中几个特殊对象。

第一个特殊的对象就是Function
js里的内置对象Object、Array、Function、Date、Math、Number、String、Boolean、RegExp等都是构造函数对象,可以通过new实例化对象出来。其proto属性都指向Function.prototype。Function这个特殊对象,是上面其他函数对象的构造函数。

这里有一条链,以Array为例:


js深入学习绕不开的原型知识_第2张图片
123.png

js中上面写的这些对象可以看成是从Function构造函数new出来的对象(实例),只不过与Object,Array构造函数 new出来的对象有点不同,实例化出来的对象是函数对象。所以有以下等式成立。

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

Array.constructor === Function // true
Object.constructor === Function // true

由于实例化的Array、Object等属于函数对象,它就有prototype属性,故给每个函数对象配了个原型,如:Array.prototype、Object.prototype,从Array、Object等实例化的对象可以完成一些相同的功能,故给这些对象内置了很多方法,让所有实例化的对象都具备这些方法,故在原型上挂载了很多方法。比如Array.prototype的方法:push、shift、unshift、concat等。
还有个特例如下:

Function.__proto__ === Function.prototype

Function 这个函数既可以看成构造函数,也可以看成实例后的函数对象。

第二个个特殊的对象就是Object.prototype
不管是构造函数、原型、还是实例化对象,其都属于对象,对象的原型最初来源都是Object.prototype这个原型对象,故:

Function.prototype.__proto__ === Object.prototype
Array.prototype.__proto__ === Object.prototype

Object.prototype.__proto__ === null

而Object.prototype这个对象的proto属性就为null了。
最后上一张我自己画的关于原型的图:

js深入学习绕不开的原型知识_第3张图片
window画图画的,忽略它的丑.png


既然说到了原型链,来说一下几个相关属性,hasOwnProperty、isPrototypeOf、in。这几个属性在原型概念中经常用到。

js的原型主要实现了属性和方法的继承,既然有继承,属性和方法就有自己的和继承来的之分。那么怎么去区分呢?

1、hasOwnProperty()方法用来判断某个对象是否含有指定的自身属性。不包括原型链上的属性

var a = {name: 'li'};
a.hasOwnProperty('hasOwnProperty'); // false
a.hasOwnProperty('name'); // true

hasOwnProperty属性继承自Object.prototype,故返回false,
name则是创建时,自带的,故返回false

2、isPrototypeOf方法测试一个对象是否存在另一个对象的原型链上。

var o = {}
var a = function () {}
var b = new a();
a.prototype = o;
var c = new a();
o.isPrototypeOf(b); // false
o.isPrototypeOf(c); // true

3、in方法也是检测一个对象中是否有每个属性。
与hasOwnProperty不同的是,它会遍历该对象上所有可枚举属性,包括原型链上的属性,有就返回true,没有就返回false;

function a() {
    this.name = 'li'
}
a.prototype = {age: 20};
var b = new a();

for (var key in b) {
    if (b.hasOwnProperty(key)) {
        console.log('自身属性'+ key);
    } else {
        console.log('继承属性'+ key);
    }
}
上面的方法经常区分一个对象中的属性是自身属性还是继承属性。

for...in 循环只遍历可枚举属性,使用内置构造函数,像 Array、Object、Number、Boolean、String等构造函数的原型上的属性都不可枚举。
如:Object.prototype.toString方法。
当然如果toString方法被重写,还是可以遍历的,如:

function animal() {
  this.name = 'lilei'
}
animal.prototype.toString = function () {
  console.log('animal say');
}
var cat = new animal();
for (var key in cat) {
  console.log(key); 
}

但是在 IE < 9 浏览器中(万恶的 IE),Object、Array等构造函数的原型上的属性即使被重写了,还是不能被枚举到。

(1)、说到可枚举,你可能想到了一个函数,没错就是propertyIsEnumerable函数,他是Object.prototype上的一个方法,他能检测一个属性在其对象上是否可以枚举。
该方法只能检测对象的自有属性,对于其原型链上的属性始终返回false,这一点要与for ... in 中的可枚举区分开。

function a() {
 this.name = 'liming';
}

a.prototype.say = function () {
 console.log(1);
}

var b = new a();

b.propertyIsEnumerable('name') // true
b.propertyIsEnumerable('say') // false

(2)、对象的属性分为可枚举和不可枚举之分,说了这么多,其实它们是由属性的enumerable值决定的。如通过Object.defineProperty函数创建的属性,可以添加该字段来决定该属性是否可枚举。

var a = {name: 'xiao ming'}
Object.defineProperty(a, "gender", {
    value: "male",
    enumerable: false
});
a.propertyIsEnumerable('name') // true
a.propertyIsEnumerable('gender') // false

(3)、到此应该已经结束了,但是我还是想提到一个函数,Object.keys。该函数返回一个对象的key的数组。看个例子

function q() {
   this.name = 'lilei'
}
q.prototype.say = function () {
   console.log('say');
}
var a = new q();
Object.defineProperty(a, "gender", {
    value: "male",
    enumerable: false
});
Object.keys(a) // ['name']

说明该方法返回该对象自有的可枚举属性。

(4)、我还想说一下JSON.stringify方法,我们都只到JSON.stringify方法可以序列化对象。你有通过它克隆对象没?

var a = {name: 'liming'}

var b = JSON.parse(JSON.stringify(a)) // {name: 'liming'}

没错对于简单的对象,我们可以这样克隆,但是他能保存对象里的所有属性吗?
我们来看一下:

function f() {
  this.name = 'lilei';
  this.like= undefined;
  this.say = function () {}
}
f.prototype.age = 20;
var a = new f();
Object.defineProperty(a, "gender", {
    value: "male",
    enumerable: false
});
var b = JSON.parse(JSON.stringify(a)) // {name: 'lilei'}

显然该方法并不能保存对象里所有属性,事实上stringify只能保存该对象自己的可枚举属性,不能保存其原型上的属性,并且自己的属性也必须满足以下要求:
1、stringify只能保存基础类型的:数字、字符串、布尔值、null四种,不支持undefined。
2、stringify方法不支持函数;
3、除了RegExp、Error对象,JSON语法支持其他所有对象;
关于其详细内容,请看这篇文章传送门

结语:你可能感觉文章后面说了一堆方法好像跟原型没多大关联,确实关联性不是很大,但是它们方法内部都涉及到了对象的属性遍历,对象属性遍历自然就联系到原型链上的属性是否可遍历,属性的可枚举性等一系列概念,所以就把它们都提了一下。

本人能力有限,以上内容为个人理解,如有错误,欢迎指正。

你可能感兴趣的:(js深入学习绕不开的原型知识)