面向对象
一、对象的定义
在ECMAscript-262中,对象被定义为“无序属性的集合,其属性可以包含基本值、对象或者函数”。无序 键值对
也就是说,在JavaScript中,对象就是由一些无序的key-value对组成。
1.创建对象
var obj1 = new Object(); var obj2 = {}; var person1 = {}; person.name = "TOM"; person.getName = function() { return this.name; } var person2 = { name: "TOM", getName: function() { return this.name; } }
以上四种方法都可以创建一个对象。
2.访问对象的属性和方法
var person = { name: 'TOM', age: '20', getName: function() { return this.name } } //访问对象的属性或方法 person.name // 或者 person['name']
当访问的属性名是个变量的时候,常常使用第二种方式。
比如,如果要同时访问name属性和age属性:
['name', 'age'].forEach(function(item) { console.log(person[item]); })
二、工厂模式
有时候对象很相似,只有一两个属性不同的时候,为了避免写过多重复性代码,就会用工厂模式的方式解决问题。
相当于提供一个模子,通过模子复制出需要的对象。
var createPerson = function(name, age) { // 声明一个中间对象,该对象就是工厂模式的模子 var o = new Object(); // 依次添加我们需要的属性与方法 o.name = name; o.age = age; o.getName = function() { return this.name; } return o; } // 创建两个实例 var perTom = createPerson('TOM', 20); var PerJake = createPerson('Jake', 22);
像这样就能创建很多个person对象。
但是有一个问题,没有办法识别对象实例的类型。
可以使用instanceof来识别。
instanceof运算符用来检测constructor.prototype是否存在于参数object的原型链上。
因此在工厂模式的基础上,需要使用构造函数的方式来解决这个麻烦。
三、构造函数
复习new操作:
- 创建一个新的对象;
- 将构造函数的this指向这个新对象;
- 指向构造函数的代码,为这个对象添加属性,方法等;
- 返回新对象。
new关键字用代码实现:
// 先一本正经的创建一个构造函数,其实该函数与普通函数并无区别 var Person = function(name, age) { this.name = name; this.age = age; this.getName = function() { return this.name; } } // 将构造函数以参数形式传入 function New(func) { // 声明一个中间对象,该对象为最终返回的实例 var res = {}; if (func.prototype !== null) { // 将实例的原型指向构造函数的原型 res.__proto__ = func.prototype; } // ret为构造函数执行的结果,这里通过apply,将构造函数内部的this指向修改为指向res,即为实例对象 var ret = func.apply(res, Array.prototype.slice.call(arguments, 1)); // 当我们在构造函数中明确指定了返回对象时,那么new的执行结果就是该返回对象 if ((typeof ret === "object" || typeof ret === "function") && ret !== null) { return ret; } // 如果没有明确指定返回对象,则默认返回res,这个res就是实例对象 return res; } // 通过new声明创建实例,这里的p1,实际接收的正是new中返回的res var p1 = New(Person, 'tom', 20); console.log(p1.getName()); // 当然,这里也可以判断出实例的类型了 console.log(p1 instanceof Person); // true
一般构造函数的首字母大写。
四、原型
工厂模式的第一个弊端是没有办法识别对象实例的类型。(所以需要用构造函数的方式来解决,用instanceof)
工厂模式的第二个弊端是,当声明了100个person对象,每个对象中的getName方法都是一模一样的,但是由于它们分别属于不同的实例,就不得不一直不停地为getName分配空间。
所以,想让每一个实例对象都访问同一个方法。
我们创建的每一个函数,都有一个prototype属性,这个属性指向一个对象。这个对象,就是原型。
当我们在创建对象时,可以根据自己的需求,选择性地将一些属性和方法通过prototype属性,挂载在原型对象上。(之前一直不太明白构造函数和原型的作用是什么)
而每个new出来的实例,都有一个__proto__属性,该属性指向构造函数的原型对象。
通过__proto__属性,可以让实例对象访问原型对象上的方法。
因此,当所有的实例都能通过__proto__属性访问到原型对象时,原型对象的方法与属性就变成了共有方法和属性。
举例:
// 声明构造函数 function Person(name, age) { this.name = name; this.age = age; } // 通过prototye属性,将方法挂载到原型对象上 Person.prototype.getName = function() { return this.name; } var p1 = new Person('tim', 10); var p2 = new Person('jak', 22); console.log(p1.getName === p2.getName); // true
通过图可以看出,构造函数的prototype属性与实例对象的__proto__属性指向同一个对象,在这个原型对象中,有原型的constructor属性,这个属性指向构造函数,还有通过prototype属性挂载的方法。
new关键字让构造函数具有了与普通函数不同的许多特点。new的过程中,执行了如下过程:
1.声明一个中间对象
2.将该中间对象的原型指向构造函数的原型
3.将构造函数的this,指向该中间对象
4.返回这个中间对象,即返回实例对象
可以知道,中间对象中的属性与方法都是在构造函数中添加的,通过this声明的属性与方法称为私有属性与方法,它们被当前某一个实例对象独有。
而通过原型声明的属性和方法,称之为共有属性与方法,可以被所有实例对象访问。
当我们访问实例对象中的属性和方法时,会优先访问实例对象自身的属性和方法,也就是私有属性与方法。
function Person(name, age) { this.name = name; this.age = age; this.getName = function() { console.log('this is constructor.'); } } Person.prototype.getName = function() { return this.name; } var p1 = new Person('tim', 10); p1.getName(); // this is constructor.
可以看出,原型中的方法没有被访问。
可以用in来判断一个对象是否有用某个属性和方法*(无论该属性/方法存在于实例对象还是原型对象):
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.getName = function() { return this.name; } var p1 = new Person('tim', 10); console.log('name' in p1); // true
in还有一个常见的应用就是判断当前页面是否能在移动端打开:
isMobile = 'ontouchstart' in document; // 很多人喜欢用浏览器UA的方式来判断,但并不是很好的方式
两种原型写法:
//以前的写法 function Person() {} Person.prototype.getName = function() {} Person.prototype.getAge = function() {} Person.prototype.sayHello = function() {} ... ... //更简单的写法 function Person() {} Person.prototype = { constructor: Person, getName: function() {}, getAge: function() {}, sayHello: function() {} }
这个Person.prototype={}其实并不是之前图中写到的Person.prototype,而是重新创建了一个{}对象,并赋值给Person.prototype,而创建的这个{}也不是最初那个原型对象。所以它并不包含constructor属性,所以必须要手动设置constructor的指向。
五、原型链
原型对象其实也是普通对象,几乎所有的对象都有可能是原型对象,也可能是实例对象,也可以同时是实例对象,同时是原型对象。这样的一个对象,正式构成原型链的一个节点。
拿toString方法举例。
这个方法是从哪里来的?
先随意声明一个函数:
function add() {}
可以用如下图片表示该函数的原型链:
可以看出,add是Function的实例,而Function的原型对象同时又是Object原型的实例。实例.__proto__==构造函数.prototype。
原型链和作用域链类似,都是一个单向的查找过程。因此,实例对象能够通过原型链,访问到处于原型链上对象的所有属性和方法。所以,add能够访问到处于Object原型对象上的toString方法。
下一章再写继承。