创建对象的方法

原型链这块最容易把人绕晕,我建议从“对象是怎么来的”开始看:常见创建对象方式大概三种。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script type="text/javascript">
// 第一种方式:字面量
var o1 = {name: 'o1'}
var o2 = new Object({name: 'o2'})
// 第二种方式:构造函数
var M = function (name) { this.name = name; }
var o3 = new M('o3')
// 第三种方式:Object.create
var p = {name: 'p'}
var o4 = Object.create(p)
console.log(o1)
console.log(o2)
console.log(o3)
console.log(o4)
</script>

打印结果:

对象是创建出来了,但你可能对结果很诧异,为什么不同呢?别急,慢慢来。

原型及原型链

先来一张容易让人懵逼的图

什么是原型对象?实例?构造函数?

概念就不多说了,看代码吧

1
2
var M = function (name) { this.name = name; }
var o3 = new M('o3')
  • 实例就是对象,在本例中 o3 就是实例,M 就是构造函数。
  • 实例通过 new 一个构造函数生成的。
  • 从上图中可以知道,实例的 __proto__ 指向的是原型对象。
  • 实例的构造函数的 prototype 也是指向的原型对象。 
  • 原型对象的 constructor 指向的是构造函数。

再来通过下面这个图来理解一下 

那什么是原型链呢?

简单理解就是:对象有 __proto__,它指向“原型对象”;原型对象本身也是对象,也有 __proto__,于是就可以一路往上找,形成一条链。直到找到 Object.prototype,这条链就到头了。

原型对象和实例之间有什么作用呢?

通过一个构造函数创建出来的多个实例,如果都要添加一个方法,给每个实例去添加并不是一个明智的选择。这时就该用上原型了。

在实例的原型上添加一个方法,这个原型的所有实例便都有了这个方法。

接着上面的例子继续演示:

1
2
3
4
5
6
7
8
9
var M = function (name) { this.name = name; }
var o3 = new M('o3')
var o5 = new M()
M.prototype.say = function () {
console.log('hello world')
};

o3.say()
o5.say()

打印结果

按照JS引擎的分析方式,在访问一个实例的属性的时候,现在实例本身中找,如果没找到就去它的原型中找,还没找到就再往上找,直到找到。这就是原型链。

补充:

只有函数有 prototype,普通对象没有(但对象有 __proto__)。

函数也有 __proto__,因为函数也是对象;它的 __proto__ 指向 Function.prototype

也就是说普通函数是Function这个构造函数的一个实例。

instanceof原理

instanceof是判断实例对象的 __proto__ 和生成该实例的构造函数的prototype是不是引用的同一个地址。

是返回true,否返回false。

注意: 实例在做 instanceof 比较时,沿着原型链往上找,只要链上出现过对应构造函数的 prototype,结果就会是 true

继续上面的代码

那怎么判断实例是由哪个构造函数生成的呢?这时候就要用到 constructor 了。

实例原型上的构造函数:obj.__proto__.constructor

new运算符

new 运算符的原理

  • 一个新对象被创建。它继承自 foo.prototype
  • 构造函数返回一个对象。在执行的时候,相应的传参会被传入,同时上下文(this)会被指定为这个新的实例。
  • new foo 等同于 new foo(), 只能用在不传递任何参数的情况
  • 如果构造函数返回了一个对象,那么这个对象会取代 new 创建出来的实例;如果没有返回对象,那么结果就是步骤 1 创建的那个对象。

下面根据 new 的工作原理,用代码手动实现一个极简版 new(只为了理解思路):

1
2
3
4
5
6
7
8
9
var new2 = function (func) {
var o = Object.create(func.prototype);    //创建对象
var k = func.call(o);             //改变this指向,把结果付给k
if (typeof k === 'object') {         //判断k的类型是不是对象
return k;                  //是,返回k
} else {
return o;                  //不是返回返回构造函数的执行结果
}
}

验证一下:

经过上图一系列折腾,不难看出,我们手动编写的 new2new 运算符的作用是一样的。

通过这个例子,你是不是已经熟知了 new 的工作原理了呢

最后回到第一节“创建对象的方法”,你会发现:创建方式不同,原型链的指向也就不同。把 prototype / __proto__ / constructor 这几个点捋顺,原型链就没那么玄学了。