构造函数创建对象
function Person() {}
var person = new Person();
person.name = 'Kevin';
console.log(person.name) // Kevin
在这个例子中,Person 就是一个构造函数,我们使用 new 创建了一个实例对象 person。
prototype
每个函数都有一个 prototype 属性,就是我们经常在各种例子中看到的那个 prototype ,比如:
function Person() {}
// 虽然写在注释里,但是你要注意:
// prototype是函数才会有的属性
Person.prototype.name = 'Kevin';
var person1 = new Person();
var person2 = new Person();
console.log(person1.name) // Kevin
console.log(person2.name) // Kevin
那这个函数的 prototype 属性到底指向的是什么呢?是这个函数的原型吗?
其实,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型,也就是这个例子中的 person1 和 person2 的原型。
那什么是原型呢?你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型”继承”属性。
让我们用一张图表示构造函数和实例原型之间的关系:
在这张图中我们用 Person.prototype 表示实例原型。
那么我们该怎么表示实例与实例原型,也就是 person 和 Person.prototype 之间的关系呢,这时候我们就要讲到第二个属性:
proto
这是每一个JavaScript对象(除了 null )都具有的一个属性,叫proto,这个属性会指向该对象的原型。
为了证明这一点,我们可以在控制台中输入:
function Person() {}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true
于是我们更新下关系图:
既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢?
constructor
指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数倒是有的,这就要讲到第三个属性:constructor,每个原型都有一个 constructor 属性指向关联的构造函数。
为了验证这一点,我们可以尝试:
function Person() {}
console.log(Person === Person.prototype.constructor); // true
所以再更新下关系图:
综上我们已经得出:
function Person() {}
var person = new Person();
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 顺便学习一个ES5的方法,可以获得对象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
了解了构造函数、实例原型、和实例之间的关系,接下来我们讲讲实例和原型的关系:
实例与原型
当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。
举个例子:
function Person() {}
Person.prototype.name = 'Kevin';
var person = new Person();
person.name = 'Daisy';
console.log(person.name) // Daisy
delete person.name;
console.log(person.name) // Kevin
在这个例子中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 Daisy。
但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.proto ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 Kevin。
但是万一还没有找到呢?原型的原型又是什么呢?
原型的原型
在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是:
var obj = new Object();
obj.name = 'Kevin'
console.log(obj.name) // Kevin
其实原型对象就是通过 Object 构造函数生成的,结合之前所讲,实例的 proto 指向构造函数的 prototype ,所以我们再更新下关系图:
原型链
那 Object.prototype 的原型呢?
null,我们可以打印:
console.log(Object.prototype.__proto__ === null) // true
然而 null 究竟代表了什么呢?
引用阮一峰老师的 《undefined与null的区别》 就是:
null 表示“没有对象”,即该处不应该有值。
所以 Object.prototype.proto 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。
所以查找属性的时候查到 Object.prototype 就可以停止查找了。
最后一张关系图也可以更新为:
顺便还要说一下,图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。
补充
constructor
首先是 constructor 属性,我们看个例子:
function Person() {}
var person = new Person();
console.log(person.constructor === Person); // true
当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以:
person.constructor === Person.prototype.constructor
proto
其次是 proto ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.proto 时,可以理解成返回了 Object.getPrototypeOf(obj)。
真的是继承吗?
最后是关于继承,前面我们讲到“每一个对象都会从原型‘继承’属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是:
继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。
prototype
首先来介绍下 prototype
属性。这是一个显式原型属性,只有函数才拥有该属性。基本上所有函数都有这个属性,但是也有一个例外
let fun = Function.prototype.bind()
复制代码
如果你以上述方法创建一个函数,那么可以发现这个函数是不具有 prototype
属性的。
prototype 如何产生的
当我们声明一个函数时,这个属性就被自动创建了。
function Foo() {}
复制代码
并且这个属性的值是一个对象(也就是原型),只有一个属性 constructor
constructor
对应着构造函数,也就是 Foo
。
constructor
constructor
是一个公有且不可枚举的属性。一旦我们改变了函数的 prototype
,那么新对象就没有这个属性了(当然可以通过原型链取到 constructor
)。
那么你肯定也有一个疑问,这个属性到底有什么用呢?其实这个属性可以说是一个历史遗留问题,在大部分情况下是没用的,在我的理解里,我认为他有两个作用:
- 让实例对象知道是什么函数构造了它
- 如果想给某些类库中的构造函数增加一些自定义的方法,就可以通过
xx.constructor.method
来扩展
这是每个对象都有的隐式原型属性,指向了创建该对象的构造函数的原型。其实这个属性指向了 [[prototype]],但是 [[prototype]] 是内部属性,我们并不能访问到,所以使用_proto_
_proto_
来访问。
因为在 JS 中是没有类的概念的,为了实现类似继承的方式,通过_proto_
将对象和原型联系起来组成原型链,得以让对象可以访问到不属于自己的属性。实例对象的
从上图可知,当我们使用_proto_
如何产生的new
操作符时,生成的实例对象拥有了_proto_
属性。
所以可以说,在function Foo() {} // 这个函数是 Function 的实例对象 // function 就是一个语法糖 // 内部调用了 new Function(...) 复制代码
new
的过程中,新对象被添加了_proto_
并且链接到构造函数的原型上。new 的过程
- 新生成了一个对象
- 链接到原型
- 绑定 this
- 返回新对象
在调用 new
的过程中会发生以上四件事情,我们也可以试着来自己实现一个 new
function create() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}
复制代码
对于实例对象来说,都是通过 new
产生的,无论是 function Foo()
还是 let a = { b : 1 }
。
对于创建一个对象来说,更推荐使用字面量的方式创建对象。因为你使用 new Object()
的方式创建对象需要通过作用域链一层层找到 Object
,但是你使用字面量的方式就没这个问题。
function Foo() {}
// function 就是个语法糖
// 内部等同于 new Function()
let a = { b: 1 }
// 这个字面量内部也是使用了 new Object()
复制代码
Function.proto === Function.prototype
对于对象来说,xx.__proto__.contrcutor
是该对象的构造函数,但是在图中我们可以发现 Function.__proto__ === Function.prototype
,难道这代表着 Function
自己产生了自己?
答案肯定是否认的,要说明这个问题我们先从 Object
说起。
从图中我们可以发现,所有对象都可以通过原型链最终找到 Object.prototype
,虽然 Object.prototype
也是一个对象,但是这个对象却不是 Object
创造的,而是引擎自己创建了 Object.prototype
。所以可以这样说,所有实例都是对象,但是对象不一定都是实例。
接下来我们来看 Function.prototype
这个特殊的对象,如果你在浏览器将这个对象打印出来,会发现这个对象其实是一个函数。
我们知道函数都是通过 new Function()
生成的,难道 Function.prototype
也是通过 new Function()
产生的吗?答案也是否定的,这个函数也是引擎自己创建的。首先引擎创建了 Object.prototype
,然后创建了 Function.prototype
,并且通过 __proto__
将两者联系了起来。这里也很好的解释了上面的一个问题,为什么 let fun = Function.prototype.bind()
没有 prototype
属性。因为 Function.prototype
是引擎创建出来的对象,引擎认为不需要给这个对象添加 prototype
属性。
所以我们又可以得出一个结论,不是所有函数都是 new Function()
产生的。
有了 Function.prototype
以后才有了 function Function()
,然后其他的构造函数都是 function Function()
生成的。
现在可以来解释 Function.__proto__ === Function.prototype
这个问题了。因为先有的 Function.prototype
以后才有的 function Function()
,所以也就不存在鸡生蛋蛋生鸡的悖论问题了。对于为什么 Function.__proto__
会等于 Function.prototype
,个人的理解是:其他所有的构造函数都可以通过原型链找到 Function.prototype
,并且 function Function()
本质也是一个函数,为了不产生混乱就将 function Function()
的 __proto__
联系到了 Function.prototype
上。
总结
Object
是所有对象的爸爸,所有对象都可以通过__proto__
找到它Function
是所有函数的爸爸,所有函数都可以通过__proto__
找到它Function.prototype
和Object.prototype
是两个特殊的对象,他们由引擎来创建- 除了以上两个特殊对象,其他对象都是通过构造器
new
出来的 - 函数的
prototype
是一个对象,也就是原型 - 对象的
__proto__
指向原型,__proto__
将对象和原型连接起来组成了原型链