忘记在哪里看到过,有人说鉴别一我的是否 js 入门的标准就是看他有没有理解 js 原型,因此第一篇总结就从这里出发。数据库
对象编程
JavaScript 是一种基于对象的编程语言,但它与通常面向对象的编程语言不一样,由于他没有类(class)的概念。设计模式
对象是什么?ECMA-262 把对象定义为:「无序属性的集合,其属性能够包含基本值、对象或者函数。」简单来讲,对象就是一系列的键值对(key-value),我习惯把键值对分为两种,属性(property)和方法(method)。安全
面向对象编程,在个人理解里是一种编程思想。这种思想的核心就是把万物都抽象成一个个对象,它并不在意数据的类型以及内容,它在意的是某个或者某种数据可以作什么,而且把数据和数据的行为封装在一块儿,构建出一个对象,而程序世界就是由这样的一个个对象构成。而类是一种设计模式,用来更好地建立对象。app
举个例子,把我本身封装成一个简单的对象,这个对象拥有个人一些属性和方法。编程语言
//构造函数建立
var klaus = new Object(); klaus.name = 'Klaus'; klaus.age = 22; klaus.job = 'developer'; klaus.introduce = function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); };
//字面量语法建立,与上面效果相同
var klaus = { name: 'Klaus', age: 22, job: 'developer', introduce: function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); } };
这个对象中,name、age 和 job 是数据部分,introduce 是数据行为部分,把这些东西都封装在一块儿就构成了一个完整的对象。这种思想不在意数据(name、age 和 job)是什么,它只在意这些数据能作什么(introduce),而且把它们封装在了一块儿(klaus 对象)。函数式编程
跑一下题,与面向对象编程相对应的编程思想是面向过程编程,它把数据和数据行为分离,分别封装成数据库和方法库。方法用来操做数据,根据输入的不一样返回不一样的结果,而且不会对输入数据以外的内容产生影响。与之相对应的设计模式就是函数式编程。函数
工厂模式建立对象this
若是建立一个简单的对象,像上面用到的两种方法就已经够了。可是若是想要建立一系列类似的对象,这种方法就太过麻烦了。因此,就顺势产生了工厂模式。spa
function createPerson(name, age, job){ var o = new Object(); o.name = name; o.age = age; o.job = job; o.introduce = function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); }; return o; } var klaus = createPerson('Klaus', 22, 'developer');
随着 JavaScript 的发展,这种模式渐渐被更简洁的构造函数模式取代了。(高程三中提到工厂模式没法解决对象识别问题,我以为彻底能够加一个_type 属性来标记对象类型)
构造函数模式建立对象
咱们能够经过建立自定义的构造函数,而后利用构造函数来建立类似的对象。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.introduce = function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); }; } var klaus = new Person('Klaus', 22, 'developer'); console.log(klaus instanceof Person); //true console.log(klaus instanceof Object); //true
如今咱们来看一下构造函数模式与工厂模式对比有什么不一样:
函数名首字母大写:这只是一种约定,写小写也彻底没问题,可是为了区别构造函数和通常函数,默认构造函数首字母都是大写。
不须要建立对象,函数最后也不须要返回建立的对象:new 操做符帮你建立对象并返回。
添加属性和方法的时候用 this:new 操做符帮你把 this 指向建立的对象。
建立的时候须要用 new 操做符来调用构造函数。
能够获取原型上的属性和方法。(下面会说)
能够用 instanceof 判断建立出的对象的类型。
new
这么看来,构造函数模式的精髓就在于这个 new 操做符上,因此这个 new 到底作了些什么呢?
建立一个空对象。
在这个空对象上调用构造函数。(因此 this 指向这个空对象)
将建立对象的内部属性__proto__指向构造函数的原型(原型,后面讲到原型会解释)。
检测调用构造函数后的返回值,若是返回值为对象(不包括 null)则 new 返回该对象,不然返回这个新建立的对象。
用代码来模仿大概是这样的:
function _new(fn){ return function(){ var o = new Object(); var result = fn.apply(o, arguments); o.__proto__ = fn.prototype; if(result && (typeof result === 'object' || typeof result === 'function')){ return result; }else{ return o; } } } var klaus = _new(Person)('Klaus', 22, 'developer');
组合使用构造函数模式和原型模式
构造函数虽然很好,可是他有一个问题,那就是建立出的每一个实例对象里的方法都是一个独立的函数,哪怕他们的内容彻底相同,这就违背了函数的复用原则,并且不能统一修改已建立实例对象里的方法,因此,原型模式应运而生。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.introduce = function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); }; } var klaus1 = new Person('Klaus', 22, 'developer'); var klaus2 = new Person('Klaus', 22, 'developer'); console.log(klaus1.introduce === klaus2.introduce); //false
什么是原型?咱们每建立一个函数,他就会自带一个原型对象,这个原型对象你能够理解为函数的一个属性(函数也是对象),这个属性的 key 为 prototype,因此你能够经过 fn.prototype 来访问它。这个原型对象除了自带一个不可枚举的指向函数自己的 constructor 属性外,和其余空对象并没有不一样。
那这个原型对象到底有什么用呢?咱们知道构造函数也是一个函数,既然是函数那它也就有本身的原型对象,既然是对象你也就能够给它添加一些属性和方法,而这个原型对象是被该构造函数全部实例所共享的,因此你就能够把这个原型对象当作一个共享仓库。下面来讲说他具体是如何共享的。
上面讲 new 操做符的时候讲过有一步,将建立对象的内部属性__proto__指向构造函数的原型,这一步才是原型共享的关键。这样你就能够在新建的实例对象里访问构造函数原型对象里的数据。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; this.introduce = this.__proto__.introduce; //这句能够省略,后面会介绍 } Person.prototype.introduce = function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); }; var klaus1 = new Person('Klaus', 22, 'developer'); var klaus2 = new Person('Klaus', 22, 'developer'); console.log(klaus1.introduce === klaus2.introduce); //true
这样,咱们就达到了函数复用的目的,并且若是你修改了原型对象里的 introduce 函数后,全部实例的 introduce 方法都会同时更新,是否是很方便呢?可是原型绝对不止是为了这么简单的目的所建立的。
咱们首先明确一点,当建立一个最简单的对象的时候,其实默认用 new 调用了 JavaScript 内置的 Objcet 构造函数,因此每一个对象都是 Object 的一个实例(用 Object.create(null) 等特殊方法建立的暂不讨论)。因此根据上面的介绍,每一个对象都有一个__proto__的属性指向 Object.prototype。这是理解下面属性查找机制的前提。
var klaus = { name: 'Klaus', age: 22, job: 'developer', introduce: function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); } }; console.log(klaus.friend); //undefined console.log(klaus.toString); //ƒ toString() { [native code] }
上面代码能够看出,若是咱们访问 klaus 对象上没有定义的属性 friend,结果返回 undefined,这个能够理解。可是一样访问没定义的 toString 方法却返回了一个函数,这是否是很奇怪呢?其实一点不奇怪,这就是 JavaScript 对象的属性查找机制。
属性查找机制:当访问某对象的某个属性的时候,若是存在该属性,则返回该属性的值,若是该对象不存在该属性,则自动查找该对象的__proto__指向的对象的此属性。若是在这个对象上找到此属性,则返回此属性的值,若是__proto__指向的对象也不存在此属性,则继续寻找__proto__指向的对象的__proto__指向的对象的此属性。这样一直查下去,直到找到 Object.prototype 对象,若是还没找到此属性,则返回 undefined。(原型链查找,讲继承时会详细讲)
理解了上面的查找机制之后,也就不难理解 klaus.toString 其实也就是 klaus.__proto__.toString,也就是 Object.prototype.toString,因此就算你没有定义依然也能够拿到一个函数。
理解了这一点之后,也就理解了上面 Person 构造函数里的那一句我为何注释了能够省略,由于访问实例的 introduce 找不到时会自动找到实例__proto__指向的对象的 introduce,也就是 Person.prototype.introduce。
这也就是原型模式的强大之处,由于你能够在每一个实例上访问到构造函数的原型对象上的属性和方法,并且能够实时修改,是否是很方便呢。
除了给原型对象添加属性和方法以外,也能够直接重写原型对象(由于原型对象本质也是一个对象),只是别忘记添加 constructor 属性。
还须要注意一点,若是原型对象共享的某属性是个引用类型值,一个实例修改该属性后,其余实例也会所以受到影响。
以及,若是用 for-in 循环来遍历属性的 key 的时候,会遍历到原型对象里的可枚举属性。
function Person(name, age, job){ this.name = name; this.age = age; this.job = job; } Person.prototype = { introduce: function(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); }, friends: ['person0', 'person1', 'person2'] }; Object.defineProperty(Person.prototype, 'constructor', { enumerable: false, value: Person }); var klaus1 = new Person('Klaus', 22, 'developer'); var klaus2 = new Person('Klaus', 22, 'developer'); console.log(klaus1.friends); //['person0', 'person1', 'person2'] klaus1.friends.push('person3'); console.log(klaus1.friends); //['person0', 'person1', 'person2', 'person3'] console.log(klaus2.friends); //['person0', 'person1', 'person2', 'person3'] for(var key in klaus1){ console.log(key); //name, age, job, introduce, friends }
ES6 class
若是你有关注最新的 ES6 的话,你会发现里面提出了一个关键字 class 的用法,难道 JavaScript 要有本身类的概念了吗?
tan90°,不存在的,这只是一个语法糖而已,上面定义的 Person 构造函数能够用 class 来改写。
class Person{ constructor(name, age, job){ this.name = name; this.age = age; this.job = job; } introduce(){ console.log('My name is ' + this.name + ', I\'m ' + this.age + ' years old.'); } } Person.prototype.friends = ['person0', 'person1', 'person2']; var klaus = new Person('Klaus', 22, 'developer');
很遗憾,ES6 明确规定 class 里只能有方法而不能有属性,因此像 friends 这样的属性可能只能在外面单独定义了。
下面简单举几个差别点,若是想详细了解能够去看阮一峰的《ECMAScript 6 入门》或者 Nicholas C. Zakas 的《Understanding ECMAScript 6》。
class 里的静态方法(相似于 introduce)是不可枚举的,而用 prototype 定义的是可枚举的。
class 里面默认使用严格模式。
class 已经不属于普通的函数了,因此不使用 new 调用会报错。
class 不存在变量提高。
class 里的方法能够加 static 关键字定义静态方法,这种静态方法就不是定义在 Person.prototype 上而是直接定义在 Person 上了,只能经过 Person.method() 调用而不会被实例共享。
做用域安全的构造函数
不论是高程仍是其余的一些资料都提到过做用域安全的构造函数这个概念,由于构造函数若是不用 new 来调用就只是一个普通的函数而已,这样在函数调用的时候 this 会指向全局(严格模式为 undefined),这样若是错误调用构造函数就会把属性和方法定义在 window 上。为了不这种状况,能够将构造函数稍加改造,先用 instanceof 检测 this 而后决定调用方法。
function Person(name, age, job){ if(this instanceof Person){ this.name = name; this.age = age; this.job = job; }else{ return new Person(name, age, job); } } var klaus1 = Person('Klaus', 22, 'developer'); var klaus2 = new Person('Klaus', 22, 'developer'); //两种方法结果同样
不过我的认为这种没什么必要,构造函数已经首字母大写来加以区分了,若是还错误调用的话那也没啥好说的了。。。