JavaScript 中的原型和原型对象

JavaScript 中的原型和原型对象

我们首先先来看一张图,如下

关于 JavaScript 中的原型,本质上来说其实就是 prototype__proto__constructor 的三者之间的关系,上图第一眼看上去感觉十分复杂,但是其实说的也就是两句话的事,如下

1
2
function Foo() { }
var f1 = new Foo()

下面我们就来逐一分析他们之间的关系

实例对象

通过构造函数的 new 操作创建的对象是实例对象,可以用一个构造函数,构造多个实例对象

1
2
3
4
5
6
function Foo() { }

var f1 = new Foo
var f2 = new Foo

console.log(f1 === f2) // false

prototype

构造函数有一个 prototype 属性,指向『实例对象的原型对象』,通过同一个构造函数实例化的多个对象具有相同的原型对象,经常使用原型对象来实现继承

1
2
3
4
5
6
7
8
9
10
11
function Foo() { }

Foo.prototype.a = 1

var f1 = new Foo
var f2 = new Foo

console.log(Foo.prototype.a) // 1

console.log(f1.a) // 1
console.log(f2.a) // 1

proto

实例对象有一个 __proto__ 属性,指向『该实例对象对应的原型对象』(需要注意,实例对象也是对象)

1
2
3
4
5
function Foo() { }

var f1 = new Foo

console.log(f1.__proto__ === Foo.prototype) // true

不过需要注意是,如果实例对象 f1 是通过 Object.create() 创建的话,结果就不一样了

1
2
3
4
5
6
function Foo() { }

var f1 = Object.create(Foo)

console.log(f1.__proto__ === Foo.prototype) // false
console.log(f1.__proto__ === Foo) // true

constructor

原型对象有一个 constructor 属性,指向『该原型对象对应的构造函数』

1
2
3
function Foo() { }

console.log(Foo.prototype.constructor === Foo) // true

由于实例对象可以继承原型对象的属性,所以实例对象也拥有 constructor 属性,同样指向原型对象对应的构造函数

1
2
3
4
function Foo() { }

var f1 = new Foo
console.log(f1.constructor === Foo) // true

constructor 属性返回对创建此对象的数组函数的引用,它是不会影响任何 JavaScript 的内部属性的

看下面一段代码

1
2
3
4
5
function Foo() { }
Foo.prototype.constructor === Foo // true

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

看起来 a.constructor === Footrue 则意味着 a 确实有一个指向 Foo.constructor 属性,但是事实不是这样,实际上,.constructor 引用同样被委托给了 Foo.prototype,而 Foo.prototype.constructor 默认指向 FooFoo.prototype.constructor 属性只是 Foo 函数在声明时的默认属性

如果你创建了一个新对象并替换了函数默认的 .prototype 对象引用,那么新对象并不会自动获得 .constructor 属性,思考下面的代码

1
2
3
4
5
6
7
8
function Foo() { }

Foo.prototype = {} // 创建一个新原型对象,这个操作相当于重写了函数的原型,不推荐这么操作

var a1 = new Foo()

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

a1 并没有 .constructor 属性,所以它会委托 prototype 链上的 Foo.prototype,但是这个对象也没有 .constructor 属性(不过默认的 Foo.prototype 对象有这个属性),所以它会继续委托,这次会委托给委托链顶端的 Object.prototype,这个对象有 .constructor 属性,指向内置的 Object() 函数,当然,你可以给 Foo.prototype 添加一个 .constructor 属性,不过这需要手动添加一个符合正常行为的不可枚举的属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Foo() { }

// 创建一个新原型对象
Foo.prototype = {}

// 需要在 Foo.prototype 上修复丢失的 .constructor 属性
// 新对象属性起到 Foo.prototype 的作用
Object.defineProperty(Foo.prototype, 'constructor', {
enumerable: false,
writable: true,
configurable: true,
// 让 .constructor 指向 Foo
value: Foo
})

实际上,对象的 .constructor 会默认指向一个函数,这个函数可以通过对象的 .prototype 引用,.constructor 并不是一个不可变属性,它是不可枚举(参见上面的代码)的,但是它的值是可写的(可以被修改),此外,你可以给任意 prototype 链中的任意对象添加一个名为 constructor 的属性或者对其进行修改,你可以任意对其赋值

所以这是一个非常不可靠并且不安全的引用,通常来说要尽量避免使用这些引用,但是有的时候,为了将实例的构造器的原型对象暴露出来,比如写了一个插件,别人得到的都是你实例化后的对象,如果想扩展下对象,就可以用 instance.constructor.prototype 去修改或扩展原型对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var a, b
(function () {
function A(arg1, arg2) {
this.a = 1
this.b = 2
}

A.prototype.log = function () {
console.log(this.a)
}

a = new A()
b = new A()
})()

a.log() // 1
b.log() // 1

因为 A 在闭包里,所以现在我们是不能直接访问 A 的,那如果我们想给类 A 增加新方法,那么就可以通过访问 constructor 就可以了

1
2
3
4
5
6
7
// a.constructor.prototype 在 chrome 和 firefox 中可以通过 a.__proto__ 直接访问
a.constructor.prototype.log2 = function () {
console.log(this.b)
}

a.log2() // 2
b.log2() // 2

或者我们想知道 a 的构造函数有几个参数?

1
a.constructor.length

或者再复杂点,我想知道 a 的构造函数的参数名是什么

1
2
a.constructor.toString().match(/\(.*\)/).pop().slice(1, -1).split(',')
// ['arg1', 'arg2']

实例与原型

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,以此类推,一直找到最顶层为止

1
2
3
4
5
6
7
8
9
10
function Foo() { }

Foo.prototype.name = 'zhangsan'

var f1 = new Foo
f1.name = 'lisi'
console.log(f1.name) // lisi

delete f1.name
console.log(f1.name) // zhangsan

在上面的例子中,我们给实例对象 f1 添加了 name 属性,当我们打印 f1.name 的时候,结果自然为 lisi,但是当我们删除了 f1name 属性时,再次读取 f1.name 的时候,就会发现在 f1 对象中已经找不到 name 属性了,所以就会从 f1 的原型,也就是 f1.__proto__ 对应的 Foo.prototype 当中去进行查找,所幸的是我们找到了 name 属性,结果为 zhangsan

但是万一还没有找到呢?原型的原型又是什么呢?

原型的原型

在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是

1
2
3
4
var obj = new Object()
obj.name = 'zhangsan'

console.log(obj.name) // zhangsan

其实原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 __proto__ 指向构造函数的 prototype,那 Object.prototype 的原型又是指向哪里的呢?我们可以打印一下

1
2
3
Object.prototype.__proto__           // null

Object.prototype.__proto__ === null // true

null 表示没有对象,即该处不应该有值,所以 Object.prototype.__proto__ 的值为 nullObject.prototype 没有原型,其实表达了一个意思,所以查找属性的时候查到 Object.prototype 就可以停止查找了,这也就是开头部分的图片当中所表达的这个意思,来看下面这个示例

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
var a = {
x: 1,
y: { z: 2 }
}

var b = {}
b.__proto__ = a

// a 和 b 都是对象(通过new Object() 生成,这里是简写)
// 所以 a.__proto__ === b.__proto__ 是相等的(都是指向 Object.prototype)
// 所以在查找 b.x 的时候先去 b 查看,发现是空对象({}),所以 b.x 是不存在的
// 然后原则上应该去 Object.prototype 上查找,但是现在把 b.__proto__ 从新指回了 a,所以应该就去 a 上查找
// 所以 b.x 为 1,b.y 为 { z: 2 }
console.log(a.x)
console.log(b.x)

// 然后把 b.x 重新赋值为 22
// 因为是基本类型,所以 a.x 是不变的
b.x = 22
console.log(a.x)
console.log(b.x)

// 这个同上面那个类似
// 但是由于是引用类型,所以 a.y 和 b.y 指向的都是同一个地址
// 其中一个变化的话自然会引起另外一个变化
b.y.z = 33
console.log(a.y.z)
console.log(b.y.z)

自定义对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 默认情况下
function Foo() { }
var foo = new Foo()
Foo.prototype.__proto__ === Object.prototype // true 理由同上

// 2. 其他情况
// 第一种情况
function Bar() { } // 这时我们想让 Foo 继承 Bar

Foo.prototype = new Bar()
Foo.prototype.__proto__ === Bar.prototype // true

// 第二种情况
// 我们不想让 Foo 继承谁,但是我们要自己重新定义 Foo.prototype(实际过程当中不建议这样操作)
Foo.prototype = {
a: 10,
b: -10
}

// 这种方式就是用了对象字面量的方式来创建一个对象,根据前文所述
Foo.prototype.__proto__ === Object.prototype // true

以上两种情况都等于完全重写了 Foo.prototype,所以 Foo.prototype.constructor 也跟着改变了,于是乎 constructor 这个属性和原来的构造函数 Foo() 也就切断了联系

基于原型的一个实例

最后我们来看一个具体的实例和它的几个变种方式来巩固一下之前的知识点,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function obj(name) {
if (name) {
this.name = name
}
return this
}

obj.prototype.name = 'name2'

var a = obj('name1')
var b = new obj

console.log(a.name) // name1
console.log(window.name) // name1

console.log(b.name) // name2

一般函数直接调用,默认 this 指向全局 window/global,通过 obj('name1') 调用,返回 this 引用,并传递给 a,此时 a 等于 window 对象,即可输出 name 值,new 操作,在没有参数的情况下 new obj 等价于 new obj() ,实例化一个对象,这时 this 指向 obj,要拿到 b.name 的值,需要保证 name 属性存在

属性查找原则是先查找当前实例有没有属性,如果有就直接使用,如果没有,就到原型上去找,在没有就接着原型链一步一步往上,这里为了和 a.name 作属性区别,使用了 if (name) 有条件的构建 this 的属性 name,所以,现在 name 属性提供给 a 使用,原型上的 name 提供给 b 使用

实例变体一

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 去掉 if 判断
function obj(name) {
// if(name) {
this.name = name
// }
return this
}

obj.prototype.name = 'name2'

var a = obj('name1')
var b = new obj

console.log(a.name) // name1
console.log(window.name) // name1
console.log(b.name) // undefined

// 这时,b 实例已经有属性 name,但是参数 name 是为 undefined 的(因为没有传递参数)
// 所以这时可以把 this.name 属性删掉,这样就能去原型找 name 了
delete b.name
console.log(b.name) // name2

实例变体二 试试传个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
function obj(name) {
this.name = name
return this
}

obj.prototype.name = 'name2'
var a = obj('name1')

var b = new obj('myname')

console.log(a.name) // name1
console.log(window.name) // name1
console.log(b.name) // myname

总结

记住以下几点

  1. 函数(Function 也是函数)是 new Function 的结果,所以函数可以作为实例对象,其构造函数是 Function(),原型对象是 Function.prototype
  2. 对象(函数也是对象)是 new Object 的结果,所以对象可以作为实例对象,其构造函数是 Object(),原型对象是 Object.prototype
  3. Object.prototype 的原型对象是 null
  4. __proto__ 是每个对象都有的一个属性,而 prototype 是函数才会有的属性
  5. __proto__ 指向的是构造该对象的构造函数的原型,而 prototype 指向的,是以当前函数作为构造函数构造出来的对象的原型对象
  6. __proto__ 并不存在于你正在使用的对象中,实际上,它和其他的常用函数(toString()isPrototypeOf(),等等)一样,存在于内置的 Object.prototype 中(它们是不可枚举的)
  7. __proto__ 看起来很像一个属性,但是实际上它更像一个 getter/setter
  8. __proto__ 的实现大致上是类似下面这样的
1
2
3
4
5
6
7
8
9
10
Object.defineProperty(Object.prototype, '__proto__', {
get: function () {
return Object.getPrototypeOf(this)
},
set: function (o) {
// ES6 中的 setPrototypeOf()
Object.setPrototypeOf(this, o)
return o
}
})

明确以下三点

  1. 通过 Function.prototype.bind 方法构造出来的函数是个例外,它没有 prototype 属性
  2. Object.prototype 这个对象,它的 __proto__ 指向的是 null
  3. 通过 Object.create(null) 创建出来的对象没有 __proto__,如下
1
2
3
var obj = Object.create(null)

obj.__proto__ // undefined

参考

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×