深入理解JS对象(一)
一、对象概述
JS中的对象是:无序属性的集合,其属性可以包含基本值、对象或者函数。
在JS中,对象看上去和JSON很像,但他们完全是两个概念。对象是在内存中真实存在的,JSON仅仅是一种数据格式规范。
ES5中可以通过构造函数来创建特定类型的对象,如Object、Array、Date、Function是已经内置的原生构造函数。我们也可以创建自定义的构造函数,之后通过new关键字调用构造函数创建对象。例如:
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.sayName = function(){ alert(this.name); }; } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor");
很显然,Person看上去跟普通函数是一样的,实际上它们就是一样,它们俩是一个东西,只有在调用的时候才区分,直接调用的是普通函数,通过new关键字调用的是构造函数。但是,通常我们会将构造函数的首字母大写,以示区分。
通过new关键字来调用构造函数生产对象会经历四个步骤:
1. 创建一个新对象。
2. 将新创建的对象设置为构造函数中的this,因此构造函数中的this就指向了新创建的对象。
3. 逐行执行构造函数中的代码。
4. 返回新创建的对象。
这四个步骤中,只有第3步是我们写入的代码,其它步骤都是由浏览器自动完成的。
上面代码中,person1和person2是Person类型的两个不同的实例对象,实例对象中有一个constructor的属性,该属性又指回Person,其实constructor属性设计的初衷就是为了标识对象类型的。我们可以用如下代码测试
alert(person1.constructor === Person); //true alert(person2.constructor === Person); //true
我们前面说了,构造函数跟普通函数是一样的,既可以用new关键字调用,又可能作为普通函数调用。
let person = new Person("Nicholas", 29, "Software Engineer"); // 当构造函数使用 person.sayName(); Person("Greg", 27, "Doctor"); // 当普通函数使用,这时候方法会被添加到 window 对象中 window.sayName();
构造函数的方式挺方便创建对象,但是它有个缺陷,就是每次创建对象时,构造函数内部的方法都会被重新创建一次。因为有this对象的存在,显然是没有必要将构造函数内部的方法对象都创建一遍,如果创建大量对象,就会存在大量的内存空间的浪费。
为了解决这个问题,我们可以将内部的方法移到全局作用域中。
function Person(name, age, job) { this.name = name; this.age = age; this.job = job; this.sayName = sayName; } function sayName() { alert(this.name); } var person1 = new Person("Nicholas", 29, "Software Engineer"); var person2 = new Person("Greg", 27, "Doctor");
这样虽然可以解决重复创建方法对象的问题,但是它导致了语义模糊。全局作用域中的东西,不应该被限制只能被某一类对象调用,这会让全局作用域名不副实。为了解决这个问题,前辈们又引入了原型模式。
二、理解原型
原型模式其实是23种设计模式中的一种,它主要是用来创建重复对象,同时又要保证性能。
JS的设计者在函数中引入了prototype(原型)属性,这个属性是一个指针,它指向原型对象,原型对象包含着特定类型所有实例共享的属性和方法。prototype既然是函数中的一个属性,那么构造函数也是函数,它也有这个属性。
从下面的例子中可以看到同类型的不同对象是可以共享原型对象中的属性和方法的。
function Person() {} Person.prototype.name = "Nicholas"; Person.prototype.age = 29; Person.prototype.job = "Software Engineer"; Person.prototype.sayName = function () { alert(this.name); }; var person1 = new Person(); person1.sayName(); //"Nicholas" var person2 = new Person(); person2.sayName(); //"Nicholas" alert(person1.sayName === person2.sayName); //true
原型应该是JavaScript中最难理解的部分,它牵涉很多内置的对象指针,让人有点头晕。
我们前面说过,构造函数的内部有一个prototype的指针,它指向该类型的原型对象。然而原型对象中又有一个constructor的指针,又指回了前面的构造函数。以Person为例,我们画个示意图如下:
可见Person.prototype指向了Person的原型对象,而原型对象中又有一个construtor指针指回了Person构造函数,即Person.prototype.constructor = Person。我们可以用代码验证这个结论的正确性。
function Person() {}
alert(Person.prototype.constructor === Person) //true
创建了自定义的构造函数之后,其原型对象默认只会取到constructor属性,至于其它方法,都是从Object继承而来的。
通过new关键字调用构造函数创建一个新的对象时,对象的内部也将包含一个prototype的内部指针,但是它跟构造函数的prototype不一样,对象的prototype指针对开发者不可见。我们可以通过如下代码验证。
function Person() {} let p = new Person(); console.log(Person.prototype) //Object console.log(p.prototype) //undefined
为了区分构造函数和实例对象中的prototype,ES5规定实例对象中的原型属性用 [[prototype]] 来标识。
但是很多浏览器都对对象中的 [[prototype]] 提供了访问方式,我们可以通过 __proto__访问到这个属性。如下:
console.log(p.__proto__) //Object
那么,构造函数、实例对象和原型对象的指向关系如下:
Person.prototype 指向原型对象,而 Person.prototype.constructor 又指回了 Person构造函数。
Person 的每个实例—— person1 和 person2 都包含一个内部属性,该属性仅仅指向了 Person.prototype;换句话说,它们与构造函数没有直接的关系。
需要强调的是对象的 [[prototype]] 这种联系是存在与实例对象和原型对象之间,不是存在于实例对象和构造函数之间。
勿在浮砂筑高台 不为繁华易匠心