深入理解JS对象和原型链

Posted by Rimin on 2019-04-21

什么是对象?

对象是一组没有特定属性的值,对象的每一个属性或方法都有一个名字,而每一个名字都映射到一个值,其中值可以是数据或函数。每一个对象都是基于一个引用类型创建的,这个引用类型可以是原生类型,也可以是开发人员自定义的类型。

JavaScript中,一切都是对象,函数也是对象,数组也是对象,但是数组是对象的子集,而对于函数来说,函数与对象之间有一种“鸡生蛋蛋生鸡”的关

所有的对象都是由Object继承而来,而Object对象却是一个函数。对象都是由函数来创建的。

比如,在控制台中
输入 typeof Object 结果是"function",

输入 typeof Function 结果还是"function"

Alt 调试结果

虽然 JS 的对象和函数的关系看起来有点迷,但是也有自己的一套体系。

从创建对象来看

JS 创建对象有很多方式,在这里不一一列出,可以看一种原型方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 function Person(){//构造函数
}

Person.prototype.name = "Nicholas";//添加原型属性
Person.prototype.age = 29;
Person.prototype.job = "software Engineer";
Person.prototype.sayName = function() {//添加原型方法
console.log(this.name);
}

var person1 = new Person(); //实例化
person1.sayName(); //“Nicholas”
var person2 = new Person(); //实例化
person2.sayName();//"Nicholas"

我们创建的每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个对象,(属性值是对象)而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。:prototype 通过 调用构造函数而创建的那个对象的原型对象。使用原型的好处可以让所有对象实例共享它所包含的属性和方法。也就是说,不必在构造函数中定义对象信息,而是可以直接将这些信息 添加到原型中。

上面的代码用图来反映:

Alt 调试结果
Alt 调试结果

对于[[Prototype]]

每一个对象都有一个这样的隐藏属性,它引用了创建这个对象的函数的prototype原型对象,我们来看一张图:
Alt 调试结果

注意:函数也是对象,自然它也有__proto__。
在控制台中,我们发现:

Alt 调试结果

⚠️⚠️ 这个指针没有标准的方法访问,IE 浏览器在脚本访问[[Prototype]]会不能识别,火狐和谷歌浏览器及其他某些浏览器均能识别。虽然可以输出,但无法获取内部信息。([[Prototype]] 也可写为__proto__)虽然无法访问到,但是可以通过: Object.isPrototypeOf(person1)判断这个实例对象是否指向它的原型对象 ;而我们也知道Person.prototype就是Object类型,即一个原型对象.

1
2
3
//承接上面的代码
Person.prototype.isPrototypeOf(person1);//true
Person.prototype.isPrototypeOf(person2);//true

上面已经初略借助对象的创建来了解了原型链相关。接下来进行更深一步的探索。

原型创建对象机制

通过原型模式创建对象,我们接触到了对象创建的机制,如图:

1
2
var o1 = new Object()
var o2 = new Object()

Alt 调试结果

接下来,再看另一种构造函数创造机制:

1
2
3
function foo() {}
var f1 = new foo()
var f2 = new foo()

image

由于一切对象(除Object.prototype)都继承自Object,所以不难理解这里最大的boss是Object.prototype,而构造它的原型对象是null,这不是证明了它就是最大的boss吗?然而,并没有那么简单,那么,function()又是怎么回事呢?继续看:

image

我们知道,一切函数都是Function的实例,而参考点击这里 ,我们发现:

Function 构造函数 创建一个新的Function对象。 在 JavaScript 中,
每个函数实际上都是一个Function对象。

全局的Function对象没有自己的属性和方法, 但是, 因为它本身也是函数,所以它也会通过原型链从Function.prototype上继承部分属性和方法。

这句话“Function从Function.prototype上继承部分属性和方法”,可以解释

1
Function.__proto__==Function.prototype

即可以说函数是由Function创建的,那么Function由自身创建,所以Function.__proto__就指向了创建它的函数(也就是自己)的prototype。

最后,再来一张总的图:

image

通过这张图我们就可以轻松知道:

1
2
3
4
5
6
7
8
9
10
Array.prototype // []
Array.prototype.__proto__ // Object

Object.prototype.__proto__ // null

Object.__proto__ // function (){}

Array.__proto__ // function (){}

Function.prototype.__proto__ // Object

函数与对象的关系,看来,最大的boss还是Object.prototype,因为从箭头的进出来看,Object.prototype是没有被谁创造出来的,而其他对象均可以找到创建它的原型对象。

原型链上的属性和方法

instanceof 判断原型和实例之间的关系

对于 A instanceof B来说,它的判断规则是:沿着A的__proto__这条线来找,同时沿着B的prototype这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回true。如果找到终点还未重合,则返回false。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 手动实现 instanceof

// 判断 obj1 是否 继承自obj2
function isInstance(obj1, obj2){
_pro = obj1.__proto__;
pro = obj2.prototype;
while(pro){
if(_pro === pro) {
return true
}
_pro = _pro.__proto__;
}
return false

}

⚠️ typeof和instanceof的区别:

  • typeof:会返回一个变量的基本类型,只有以下几种: number ,boolean,string,object,undefined,function,但是typeof在判断到引用类型的时候,返回值只有object/function,你不知道它到底是一个object对象,还是数组,还是new Number等等。在 JS数据类型 中介绍了好几种方法对更细致的数据类型进行判断
  • instanceof 判断原型和实例之间的关系
原型链上的属性设置与屏蔽

我们已经知道原型链的搜索机制,那么假如我们人为地利用原型链改变对象之前的继承,即手动搭建原型链,会出现什么结果呢?

比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Sup(){
this.a = 1;
}
Sup.prototype.fn1 = function(){
return true;
}
function Sec(){
this.b = 2;
}
Sec.protoype = new Sup();//继承了Sup()
console.log(Sec.protoype.fn1);
Sec.prototype.fn2 = function(){
return false;
}
Sec.prototype.fn1 = function(){
return false;
}
var instance = new Sec();
console.log(instance.fn1);

实际上, 不管有没有改变其原来的继承,重写都会屏蔽,可以看这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Sup(){
this.a = 1;
}
Sup.prototype.fn1 = function(){
return true;
}
function Sec(){
this.b = 2;
}
Sec.prototype = new Sup();
Sec.prototype.fn2 = function(){
return false;
}
Sec.prototype.a = 4;
var instance = new Sec();
instance.c = 3;
console.log(instance);

这里要注意的是,在添加属性时,原型链仍有一定的搜索机制,而不是直接添加:

  • 如果属性c不是直接存于instance上,[[Prototype]]链就会被遍历,如果[[Prototype]]链上找不到c,c这时就会被直接添加到instance上。
  • 然而,如果这时属性a存在于原型链上层而不存在于instance中,赋值语句instance.a = 4却不会修改到Sec.prototype中的a,而是直接把a作为一个新属性添加到了instance上
    那么为什么在添加或修改时最终都是添加到是实例中,为何还要搜索,是因为可能存在着三种情况:

先来看一下代码(顺便我们转换一下思维,之前都是有构造函数后绑定原型.prototype,现在是有实例对象后绑定原型对象 .proto

1
2
3
4
5
6
7
8
9
10
11
12
var parentObject = {
a: 1,
b: 2
};
var childObject = {};

childObject.__proto__ = parentObject;

childObject.c = 3;
childObject.a = 2;
console.log(parentObject); // Object {a: 1, b: 2}
console.log(childObject); // > Object {c: 3, a: 2}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var parentObject = {};
Object.defineProperty(parentObject, "a", {
value: 2,
writable: false, // 标记为不可写
enumerable: true
});

var childObject = {
b: 3
};

childObject.__proto__ = parentObject; // 绑定原型
childObject.a = 10; //试图改变

console.log(childObject.a); // 2
console.log(childObject); // > Object {b: 3}
console.log(parentObject); // Object {a: 2}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var parentObject = {
set a(val) {
this.aaaaaa = val * 2;
}
};

var childObject = {
b: 3
};

childObject.__proto__ = parentObject;
childObject.a = 10;

console.log(childObject); // ?
console.log(parentObject); // ?

结果是:
image

发现,10被当成参数传进原型对象中,而实例对象则得到对应的aaaaaa,这又该怎么解释呢?

这就要了解一下set 函数了,这个set函数的运行机制就是如此。

所以就有这三种情况

  1. 如果在[[Prototype]]链上层存在名为a的普通数据访问属性,并且没有被标记为只读(writable: false),那就会直接在childObject中添加一个名为a的新属性,它是屏蔽属性,这个情况就是上文例子中发生的情况。
  2. 如果在[[Prototype]]链上层存在a,但它被标记为只读(writable: false),那么无法修改已有属性或者在childObject上创建屏蔽属性,严格模式下执行这个操作还会抛出错误。
  3. 如果在[[Prototype]]链上层存在a,但它被标记为只读(writable: false),那么无法修改已有属性或者在childObject上创建屏蔽属性,严格模式下执行这个操作还会抛出错误。

如此一来,其添加或重写属性时就仍要按原型链搜索。

原型链上的constructot属性

constructor属性指向prototype属性所在函数

比如:

1
2
3
4
5
6
7
8
function Foo() {
this.name = 'dog';
}

Foo.prototype.constructor === Foo; // true

var a = new Foo();
a.constructor === Foo; // true

当a.constructor === Foo的时候,其实这时候并不能够说明a是由Foo构造而成的。实际上,a.constructor的引用是被委托给了Foo.prototype(本身a自身是没有这个属性的),所以才会出现等价的情况,而并不能说明a是由Foo构造而成的。

再看下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Foo() {
this.name = 'dog';
}
Foo.prototype = { // 字面量方法
h: 'hhh'
};

var a1 = new Foo();

a1.constructor === Foo; // false
a1.constructor === Object; // true

a1 instanceof Foo; //true

这里由于Foo.prototype的默认属性被清空了,所以constructor不存在,可是__proto__构成的原型链是不变的,所以a1.constructor的引用被委托到Object.prototype.constructor,所以第一个返回false,第二个返回true。

constructor作为原型链中的一环,会影响原型链的指向,但是它只是一个默认的属性,而且因为原型链是一个松散连接的结构,constructor并不安全。

对new操作符的认识

当执行new操作符时

  1. 创建一个全新的空对象
  2. 这个新对象会被执行[[prototype]]连接,使其沿着原型链依次继承各级原型对象的属性。
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

将以上步骤拆解对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function SuperType(name) { // 定义了一个超类,供下面的子类继承
this.name = name;
}

function SubType() { // 定义了子类1,继承了超类,无返回值
SuperType.call(this, "Cong1");
this.age = 29;
}

function SubType2() { // 定义了子类2,继承了超类,返回了一个引用类型的值
SuperType.call(this, "Cong2");
this.age = 29;
return { a: 2 };
}

function SubType3() { // 定义了子类3,继承了超类,返回了一个值类型的值
SuperType.call(this, "Cong3");
this.age = 29;
return 3;
}
/* 下面比较有new操作符和无new操作符调用子类的区别 */

var instance1_nonew = SubType();
var instance2_nonew = SubType2();
var instance3_nonew = SubType3();
var instance1_hasnew = new SubType();
var instance2_hasnew = new SubType2();
var instance3_hasnew = new SubType3();


// 依次打印六个变量
console.log(…);

得到的结果:

1
2
3
4
5
6
7
8
9
10
11
12
instance1_nonew
undefined
instance2_nonew
> Object {a: 2}
instance3_nonew
3
instance1_hasnew
> SubType {name: "Cong1", age: 29}
instance2_hasnew
> Object {a: 2}
instance3_hasnew
> SubType3 {name: "Cong3", age: 29}

首先,从得到的结果来看,前三种情况很容易想到,因为只是将构造函数赋给另一个变量而已,当然得到它的返回值,而第四种情况也较常见,但是最后两种情况比较少见,总结一下:

  1. 首先新建一个对象:
    var instance = new Object()
  2. 给这个对象设置[[prototype]]链: instance.proto = SubType.prototype
  3. 绑定this,将SubType中的this指向instance,执行SubType中的语句进行赋值。
  4. 返回值,这里要根据SubType的返回类型来判断:
  • 如果是一个引用类型(对象),那么就替换掉instance本身的这个对象。(如:instance2_hasnew)
  • 如果是值类型,那么直接丢弃它,返回instance对象本身。(如:instance3_hasnew)

(不过很少有在构造函数直接返回值的情况)

  • 手动实现 new 操作:
1
2
3
4
5
6
function newOperation(SubType){
var instance = new Object();
instance.__proto_ = SubType.prototype;
let res = SubType.call(instance)
return typeof res === "object" ? res : instance
}
  • jquery之无new构建
  1. 最基本的创建对象的做法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var jQuery = function () {
// 构造函数
}

jQuery.prototype = {
version: '1.1.1',
name: function() {
console.log(this.version);
}
}

var a = new jQuery();

a.name(); // "1.1.1"

但是,jquery 是没有 new 操作符的,那么如何不用 new 操作符呢?
我们可能会想到这么做:

1
2
3
4
5
6
7
8
9
10
11
12
var jQuery = function(selector, context) {
return new jQuery(selector, context);
}

jQuery.prototype = {
name: function() {
// name方法
}
}


jQuery();

然而,这里出现了死循环。

接下来,我们可能会这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
var jQuery = function (selector, context) {
return new jQuery.prototype.init(selector, context);
}

jQuery.prototype = {
constructor: jQuery,
init: function(selector, context) {
return this;
},
name: function () {
console.log("name调用");
}
}

但是,这里JQuery.xxx会出错,因为明显所 new 的 jQuery.prototype.init中返回的this的指向已经被隐式修改。

所以,我们需要这么做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var jQuery = function (selector, context) {
return new jQuery.prototype.init(selector, context);
}

jQuery.fn = jQuery.prototype = {
constructor: jQuery,
init: function(selector, context) {
return this;
},
name: function () {
console.log("name调用");
}
}

jQuery.fn.init.prototype = jQuery.prototype;

jQuery().name(); // name调用

其实我们把JQuery的 constrctor 和 prototype 都做了手动的控制,让它“伪造”成一个被我们自己new 出来的对象。

以上就是JS原型链和原型链的主要内容。