除了6种原始数据类型,JavaScript中的其他数据皆为对象。JavaScript的继承是由原型链(prototype chain)来实现的。
每一个JavaScript对象都有一个私有对象指向另外一个对象,也就是原型(prototype)。根据ES6的规定,我们可以用 someObject.[[Prototype]]
来表示 someObject
对象的原型。 [[Prototype]] 对象可以通过 Object.getPrototypeOf()
方法和Object.setPrototypeOf()
方法来获取和设定新对象。我们也经常用 someObject.__proto__
方式来操作原型,该方法不是JavaScript的标准,也不被官方推荐使用,不过大部分浏览器都是支持的。
我们通过__proto__
属性来获取对象的原型对象,原型对象又可以通过__proto__
属性来获取它的原型对象,这样,我们通过不断调用__proto__
属性,就可以获取的层层的原型对象,这也就是我们所说的原型链。几乎所有的对象,其原型链末端都指向Object.prototype
。
var arr = [];
arr.__proto__ === Array.prototype;
arr.__proto__.__proto__ === Object.prototype
arr.__proto__.__proto__.__proto__ === null
我们创建一个对象的过程是怎样的呢,下面举例说明
function Human(name, age) {
this.name = name || '';
this.age = age || 0;
}
var p = new Human('zhang san', 20);
p.name // 'zhang san'
p.age // 20
这里,我们通过构造函数创建了一个新的对象p
,那这个对象创建的具体过程是怎样的呢,仅以上述代码为例,具体的创建过程如下:
- 创建一个空对象
- 将新对象的原型绑定到Human的prototype属性上
- 调用构造函数Human,并将this绑定到新对象上
- 判断返回值,如果返回值不是一个Object对象,就返回第一步中创建的对象,否则返回该Object对象,第一步中创建的对象也将被弃用。当然一般我们不会在构造函数中返回其他对象。
用代码表示就是:
var obj = new Object();
obj.__proto__ = Human.prototype;
Human.call(obj, 'zhang san', 20);
var p = Human() instanceof Object ? Human() : obj;
这里需要注意的是,构造函数的prototype
属性和对象的__proto__
属性不是一个概念。除了Object,每个对象都有一个__proto__
属性;每个函数都有一个prototype
属性,函数也是对象,因此也有__proto__
属性。
函数的prototype
属性包含两个属性:
-
constructor
属性指向函数对象自身; -
__proto__
属性代表该函数对象的原型
构造函数其实就是普通函数,只不过构造函数的用途是创建对象。我们知道,JavaScript中除了6个基本数据类型,其他都是对象,函数也不例外,函数就是可被调用的对象。
对象的原型也是一个对象,根据上面的分析我们可以发现,当我们创建一个新的对象时,对象的__proto__
属性指向了特定的原型对象,不管我们通过Human函数创建多少个对象,其原型都是同一对象,即Human.prototype
。
let p1 = new Human;
let p2 = new Human;
p1.__proto__ === p2.__proto__; // logs: true
// 改变原型对象时,所有继承自该原型的对象都将收到影响
Human.prototype.a = 1;
p1.a === 1; // logs: true
p2.a === 1 // logs: true
当我们想要获取对象某个属性的值时,JavaScript会执行以下操作:
- 首先会去查询该对象自身的属性,查找到了就返回值
- 如果对象自身不存在该属性,就去原型链中查找,如果原型链中有这个属性,就返回这个属性的值。
- 如果原型链中也没有这个属性,JavaScript就会认为不存在该属性。
如果对象和原型链中都有同一个属性,那我们默认查到的会是对象自身的属性。
let person = new Human;
Human.prototype.country = 'China';
console.log(person.country); // logs: China
person.country = 'United States'; // 这一步创建了对象自己的属性
console.log(person.country); // logs: United States
console.log(person.__proto__.country) // logs: China
现在我们知道对象跟构造函数的关系了,那如果我们创建对象后,又改变了构造函数自身的属性,会对已生成的对象产生影响吗?一般而言,没大影响。从对象的创建过程我们可以看出,只有绑定到this的属性会在对象创建的过程中起作用,其他对象不会对对象的创建产生影响。但我们也知道对象的__proto__
属性其实就是函数的prototype
属性,而prototype
属性中的constructor
属性就指向函数本身,因此我们依然可以以一种曲径通幽的方式访问到函数自身的属性。
let libai = new Human('libai', 1318);
console.log(libai); // logs: HUman {name: 'libai', age: 1318}
Human.address = 'shandong';
console.log(libai.address); // logs: undefined
console.log(libai.__proto__.constructor.address) // logs: shandong
通过构造函数创建的对象,继承的属性只是函数prototype属性指向的对象的属性,当我们想要修改对象原型的属性,我们只能在修改prototype对象上的属性。
let libai = new Human('libai', 1318);
Human.prototype.address = 'china';
console.log(libai.address); // logs: china
除了通过构造函数创建对象,我们还可以通过Object.create()方法创建对象。通过Object.create()方法,我们可以不用再通过创建构造函数来创建对象,而是直接通过对象来创建对象。
let female = new Human('female');
let f = Object.create(female);
f.__proto__ === female; // logs: true
这里我们不难看出,对象f
的原型对象就是female
,而不是构造函数的prototype
属性。对象f
的原型链如下:
f.__proto__ === female;
f.__proto__.__proto__ === Human.prototype;
f.__proto__.__proto__.__proto__ === Object.prototype;
f.__proto__.__proto__.__proto__.__proto__ === null;
网友评论