面向对象的程序设计---创建对象/继承/函数/闭包
一、创建对象的几种方式
1.工厂模式
工厂模式:定义一个"工厂函数",每次调用这个函数就会得到一个具体的拥有特定属性和方法的对象。抽象了创建具体对象的过程。
写法:函数内通过new object()显示的创建一个对象,为其添加属性和方法;最后返回这个对象。
function creatPeople (name,age) { var o=new object () o.name=name; o.age=age; o.sayName = function(){ alert(this.name); }; return o; } var person1 = createPerson("Nicholas", 29);
//person1相当于:
{
name:'Nicholas',
age:29
}
//用typeof检测,person1是Object类型
优点:可以创建多个具有相同属性和方法的相似的对象
不足:无法确定一个对象的类型(Object对象、Array类型的对象、Function类型的对象、Date类型的对象等)
2.构造函数模式
通过构造函数生成对象:先写一个构造函数---通过new 这个构造函数,创建一个实例对象(它是构造函数的原型的孩子)
写法:函数内直接将属性和方法赋给this对象;没有return语句;通过new一个构造函数(用来创建一个对象)来实例化一个对象。
function Person(name, age){ this.name = name; this.age = age; this.sayName = function(){ alert(this.name); };
//this指的是这个构造函数的prototype,即原型Person.prototype
//每个函数都有一个prototype属性;每个对象都有一个__proto__属性,都指向原型
//每个原型都有一个constructor属性,指向构造函数 } var person1 = new Person("Nicholas", 29);
//实例对象的__proto__就是实例对象的构造函数的prototype 即person1.__proto__===Person.prototype
//实例对象的原型的构造函数是:person1.constructor===Person()
优点:可以确定一个对象的类型
创建的自定义的构造函数,可以将它的实例标识为一种特定的类型 (如上述示例中:实例对象person1是有类型而言的,它是一个Person类型,同时它也是Object类型的一个实例-万物皆对象)
注意一个规则:凡是通过new来调用的函数,会被当做一个构造函数。否则就是一个普通的函数。
缺点:每实例化一个对象,就相当于调用一次构造函数,每次调用就会创建一次函数中完成同样任务的的方法,造成资源的浪费 (这显然是没必要的,怎么做让实例化多个对象时只创建一次方法呢?)
每个实例都包含一个不同的Function实例(以区别name属性,里面的方法也是不等价的)-以下是证明
//1.首先, this.sayName=function(){}其实经历了以下两步: var temp=new Function() this.sayName=temp //所以,每次调用构造函数,都会创建一个函数实例(Function类型的对象实例) //2.其次,通过测试 var person1=new Person('mike',10) var person2=new Person('anmy',20) console.log( person1.sayName=== person2.sayName) //结果是false
这个问题可以通过把函数定义到外面解决。在构造函数外面只声明一次函数,如下:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayName = sayName;
}
function sayName(){ alert(this.name); } var person1 = new Person("Nicholas", 29); var person2 = new Person("Greg", 27); //此时sayName 包含的是一个指向函数的指针,person1 和 person2 对象就共享了在全局作用域中定义的同一个 sayName()函数。
//可是新问题又来了:在全局作用域中定义的函数实际上只能被某个对象调用,这让全局作用域有点名不副实。
//此外如果对象需要定义很多方法,那么就要定义很多个全局函数,于是我们这个自定义的引用类型就丝毫没有封装性可言了。
如何解决上述问题呢?
3.原型模式
因为每创建一个函数都有一个prototype属性,这个属性是一个指针,指向一个对象,也就是父亲。
这个父亲拥有的所有的属性和方法都可以被它的孩子继承。那么给这个父亲添加的所有的属性和方法,其实也是在给孩子添加属性和方法。
function Person(){ } Person.prototype.name = "Nicholas"; Person.prototype.age = 29; 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
//person1、person2作为一个实例对象是通过Person()构造出来的。他们访问的是同一组属性和方法,即值都是一样的,这些属性是继承自他们的父亲Person.prototype的。
//如果我们给这些属性值重新赋值,其实并不是改变原型中的这些属性值,而是覆盖掉这些值,原型中的属性并不会改变。
//当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。
person1.name=‘haha’
console.log(person1.name) //haha
var person3 = new Person();
console.log(person3.name) //'Nicholas'
//如果我们想重新访问到原型中的的属性,只需要把这个实例的属性delete掉就可以。
delete person1.name
//使用 hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中
//只有重写原型中的属性,返回true,证明是实例中的属性,否则返回false,证明是原型中的属性
console.log(person1.hasOwnProperty('name')) //true
console.log(person3.hasOwnProperty('name')) //false
3.1 in操作符
in操作符在通过对象能够访问到给定的属性时,返回true。其中既包括存在于实例中的属性,也包括存在于原型中的属性。
同时使用hasOwnProperty()和In操作符可以确定属性是存在于对象中还是原型中:
function whereProperty(object,pro){ console.log(!object.hasOwnProperty(pro)&&(pro in object)) }
由于 in 操作符只要通过对象能够访问到属性就返回 true, hasOwnProperty()只在属性存在于实例中时才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以确定属性是原型中的属性。通过for in,遍历对象所能访问到的可枚举属性(实例和原型中)
通过Object. keys()方法,接收一个对象作为参数,返回一个包含对象的所有可枚举的自有属性的字符串数组。
var objKey={name:'jin',age:20} console.log(Object.keys(objKey)) //name age
通过 Object.getOwnPropertyNames()方法 可以列举实例的所有属性,无论是否可以枚举。
3.2原型模式的简写
前面的原型模式的例子可以简写成如下样式:-用对象字面量重写原型对象
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, job: "Software Engineer", sayName : function () { alert(this.name); } };
这样写,相当于给Person的原型重新赋值了,原来的写法只是给Person的原型添加属性,这是两种不用的概念。我们在
这里使用的语法,本质上完全重写了默认的 prototype 对象(相当于new Object()),因此 constructor 属性也就变成了新对象的 constructor 属性(指向 Object 构造函数),不再指向 Person 函数。
如果我们希望以后通过这个构造函数创建的实例对象,可以访问constructor,并且依然指向Person,那我们在给原型赋值的时候,设置一下constructor。如下:
function Person(){ } Person.prototype = { constructor : Person, name : "Nicholas", age : 29, sayName : function () { alert(this.name); } };
上述方法直接写明constructor属性,会使其变成可枚举的属性,而默认是不可枚举的,如果想要原来的不可枚举的效果,可用Object.defineProperty()。
function Person(){ } Person.prototype = { name : "Nicholas", age : 29, sayName : function () { alert(this.name); } }; Object.defineProperty(Person.prototype, "constructor", { enumerable: false, value: Person });
3.2.1原型的动态性
保证了对原型对象所做的任何修改都会立刻反应到实例对象上,即使是先创建了实例对象后修改原型
对原型的操作(如添加属性、方法)都是动态的,不管孩子(实例对象)是什么时候创建的,只要父亲(原型)变了,孩子就会跟着变。
var person4=new Person() Person.prototype.sayHi = function(){ alert("hi"); } person4.sayHi() //hi
若重写整个原型对象,就无法在实例对象中得到反馈了function Person(){
} var friend = new Person(); Person.prototype = { constructor: Person, name : "Nicholas", age : 29, sayName : function () { alert(this.name); } }; friend.sayName(); //error
//调用实例时会为实例添加一个指向最初原型的__proto__指针,而把原型修改为另一个对象,就切断了构造函数与原始原型的联系,切断了现有原型与任何之前已经存在的对象实例之间的联系
因为此时的原型的constructor不在指向Person(),而是指向Object()
//实例的对象指针仅仅指向原型,而不是指向构造函数
//此时friend 指向的原型中不包含以该名字命名的属性,这些实例引用的仍是最初的原型,所以会报错
缺点:使用原型模式实例化对象时,所有的对象都会有统一的属性和方法,这样一来,就无法区分了,也就没有定制性了。
3.2.2原型对象的问题
除上述问题,原型模式最大的问题是由其共享的本质决定的
对于普通的属性,通过在实例上添加一个同名属性,可以隐藏原型中的对应属性。然而,对于包含引用类型值的属性来说,问题就比较突出了
function Person(){ } Person.prototype = { constructor: Person, name : "Nicholas", age : 29, friends : ["Shelby", "Court"], sayName : function () { alert(this.name); } }; var person1 = new Person(); var person2 = new Person(); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Court,Van" alert(person2.friends); //"Shelby,Court,Van" alert(person1.friends === person2.friends); //true
// 若我们的初衷就是像这样在所有实例中共享一个数组,那么这么做是对的。可是,实例一般都是要有属于自己的全部属性的。而不是某个实例特有的属性别的实例也有。
如何解决上述问题?
4.混合模式(构造函数模式+原型模式)
构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。
function Person(name, age, job){ this.name = name; this.age = age; this.friends = ["Shelby", "Court"]; } Person.prototype = { constructor : Person, sayName : function(){ alert(this.name); } } var person1 = new Person("Nicholas", 29); var person2 = new Person("Greg", 27); person1.friends.push("Van"); alert(person1.friends); //"Shelby,Count,Van" alert(person2.friends); //"Shelby,Count" alert(person1.friends === person2.friends); //false alert(person1.sayName === person2.sayName); //true
构造函数定义,定义的是什么?
是当前构造函数可生成的实例的属性和方法
原型定义,定义的是什么?
是原型的属性和方法,共享于每个实例。
5.动态原型模式
把所有信息都封装在构造函数内,通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
注意:使用此模式,不能使用对象字面量重写原型,这会切断实例与现有原型的联系。function Person(name, age, job){ //属性
this.name = name; this.age = age; //方法 if (typeof this.sayName != "function"){ Person.prototype.sayName = function(){ alert(this.name); }; }
//这里只在 sayName()方法不存在的情况下,才会将它添加到原型中.
} var friend = new Person("Nicholas", 29, "Software Engineer"); friend.sayName();
6.寄生构造函数模式
function Person(name, age){ var o = new Object(); o.name = name; o.age = age; o.sayName = function(){ alert(this.name); }; return o; } var friend = new Person("Nicholas", 29); friend.sayName(); //"Nicholas
7.稳妥构造函数模式
稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象 。
一是新创建对象的实例方法不引用 this;二是不使用 new 操作符调用构造函数 。
function Person(name, age, job){ //创建要返回的对象 var o = new Object(); //可以在这里定义私有变量和函数 //添加方法 o.sayName = function(){ alert(name); }; //返回对象 return o; } var friend = Person("Nicholas", 29); friend.sayName(); //"Nicholas"
//变量 friend 中保存的是一个稳妥对象,而除了调用 sayName()方法外,没有别的方式可以访问其数据成员
二、继承
继承:利用原型链实现继承。若a继承b,实际是创建b的实例,并将其赋给a.prototype,即a.prototype=new B() 实现的本质是重写原型对象,代之以一个新类型的实例。
下面先简单介绍一下什么是原型链。
1.原型链
简言之,让一个原型对象等于另一个类型的实例,此时原型对象将包含一个指向另一个原型的指针,相应的另一个原型包含一个指向另一个构造函数的内部指针(此时这个实例对象的constructor指向的是另一个原型的constructor)。(另一个类型的实例属性也将变成原型属性了。)
假如另一个实例的原型又等于另一个类型的实例,那么上述关系依然成立,如此层层递进,就形成了实例与原型之间的链条,这就是原型链的思想。
(所有函数的默认原型都是Object的实例,因此默认原型都会包含一个指针,指向Object.prototype )
如果原型链没有任何问题的话,继承实际就是:所有的实例继承其原型,或原型链上面的所有父原型。
但是,原型链有个问题。-来自包含引用类型值的原型
原型中定义的属性,会被所有实例共享,可以通过在实例对象里覆盖掉这个属性。-针对基本数据类型
原型中定义的“引用类型的值”的属性,也会被所有实例共享。
那什么是“引用类型的值”呢?
ECMAScript变量包含两种数据类型的值:基本数据类型、引用数据类型。基本数据类型的值指的是简单的数据段;引用数据类型值指的是那些可能由多个值构成的对象。引用类型的值是保存在内存中的对象。与其他语言不同,JavaScript不允许直接访问内存中的位置,也就是说不能直接操作对象的内存空间,在操作对象时,实际上操作的是引用而不是实际的对象,为此,引用类型的值是按引用访问的。
换句话说,如果我们在原型里定义一个属性-数组类型的,那实例对象继承的这个属性其实是这个属性的引用,更改这个实例对象中的数组,意味着更改引用,原型中的数组也会变。继而由该构造函数创建的所有实例都会拥有改变后的数组值。
问题:引用类型的值所带来的问题。
那么如何解决这个问题呢?
2.借用构造函数
借用构造函数,其实就是在孩子的构造函数中调用父亲(原型)的构造函数。这样,就把父亲构造函数中定义的所有代码都在子类的构造函数中执行了一遍。
function Father(){ this.colors = ["red", "blue", "green"]; } function Child(){ //继承了 SuperType Father.call(this); } var instance1 = new Child(); instance1.colors.push("black"); alert(instance1.colors); //"red,blue,green,black" var instance2 = new Child(); alert(instance2.colors); //"red,blue,green"
//通过Child new一个对象实例,那这个对象实例就拥有了color这个属性,而且是独自拥有color的拷贝。
问题:方法都定义在构造函数中,每次创建一个实例都要调用一次函数,创建一次方法,资源浪费,不可复用。
父类原型中的方法,孩子中无法获取。
那么如何解决这个问题呢?
3.组合继承
原型链+借用构造函数模式。思路是:使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。
function Father(name){ this.name = name; this.colors = ["red", "blue", "green"]; } function Child(name, age){ //继承属性 Father.call(this, name); this.age = age; } Father.prototype.sayName = function(){ alert(this.name); } //创建父构造函数
//创建子构造函数,并继承父类中的name属性
//给父类的原型添加一个方法sayName
Child.prototype=new Father() //给子类型添加一个原型,这个原型就是父类型的实例
Child.prototype.constructor=Child //确定通过子类生成的实例对象是Child类型
//到这里,所有通过new Child()创建的实例对象,都拥有了sayName方法,各自拥有color,name,age属性
4.原型式继承/寄生式继承
通过现有的对象创建一个新的对象,并为其添加方法。
本质就是:创建一个把现有对象当做原型的构造函数,然后通过这个构造函数创建一个孩子。
首先思考一个问题?
按照前面的那些方式,到底创建一个继承于父类的对象实例的本质是什么?
本质很简单:按照父亲创造出孩子,不仅要保证每个孩子都有自己的个性,还要保证每个孩子一样的地方不需要重复创造,而且单个孩子的某个行动,不会影响到父亲以至于波及到其他孩子。
逐条分析:
1.保证每个孩子有自己的个性。
孩子的构造函数就是干这个事的,每个孩子都有自己的独有属性,这些独有属性就在构造函数里写,其他的都在父亲(原型)中继承。
2. 保证孩子一样的地方不需要重复创造。
每个孩子都会说话、吃饭、睡觉,这些不必要在孩子的构造函数里写,只需要在父亲(原型)里写就可以了。
3. 不会影响到父亲波及其他孩子。
引用类型值的属性。这些属性如果是继承的,那一个孩子更改了这个属性,这个父亲的所有孩子都会改变了。因为所有的孩子里的这个属性,都是引用,而不是值。
能不能通过现有的对象,直接创建一个新对象呢?
function child(FatherIns){ function F(){} F.prototype = FaherIns return new F() } //本质是创建一个把FahterIns当作原型的 构造函数 //然后通过这个构造函数创建一个孩子
其实这种继承方式的本质是:对象的深拷贝。而并非严格的继承。所以,这种继承方式的前提是:1.有现成继承的对象,2.不需要考虑类型 3.现有对象中如果存在引用类型值属性,将会被所有孩子继承。
于是,ES5为此给Object增添了一个新方法:Object.create()用来创建新对象,接收两个参数:1.用作新对象原型的对象,2.一个为新对象定义额外属性的对象。
能不能给生成的对象添加方法呢?
function child(fatherObj){ var tmp = Object.create(fatherObj,{ childPro:{ value:'xxxxx' } }) tmp.childMethod = function(){ ... } return tmp; } //这其实就是寄生式继承
5.寄生组合式继承
回看一下 组合式继承的思路:
1. 创建子类型的构造函数。
2. 在构造函数中,调用父类的构造函数。 //第二次调用
3. 定义完构造函数之后,外面还要给子类型指定原型:Child.prototype = new Father() //第一次调用
4. 我们都知道指定原型造成的弊端就是失去constructor。所以再指定一下constructor. Child.prototype.constructor = Child
5. 这时候继承定义完成。
这时候我们发现,Father()这个构造函数调用了两次。而且,Child的prototype我们其实是不关心它的类型的。并且,Child.prototype可不可以从一个现有的对象创建呢?完全可以啊。那这个现有的对象就是Father.prototype啊。
所以我们就可以把3、4步写成:
var prototype = Object.create(Father.prototype) Child.prototype = prototype prototype.constructor = Child
这里并没有给Child一个通过Father()新建的实例,而是通过Father.prototype拷贝的实例。因为这个实例的类型并不是我们关心的。
三、函数
3.1执行环境和作用域
执行环境 定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象,环境中定义的所有变量和函数都保存在这个对象中。
每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。
关键是:执行
function f(x,y){ var z = x+y console.log(z) }
这里是函数定义。不涉及执行环境。
f(2,3)
这里是函数执行。在执行这条语句时,会发生下面的事。
1. 创建一个函数执行环境和一个环境对象。 {argument:{x:,y:},z:}
2. 进入函数体,这里面的x,y,z都是可访问的,因为在环境对象里有。如果这个执行环境里没有,就去更外面的执行环境里找,比如window对象。
这时候就出来两点:1.外面不能访问执行环境里面的对象属性。2.执行环境不能向下搜索,即如果在执行函数里再定义一个函数,那里面定义的变量,外面同样不能访问。
最重要的一点:函数执行完之后,执行环境销毁,储存变量的对象同样被销毁。 (执行环境会随着函数的调用和返回,不断地重建和销毁。)
若变量对象在有变量引用(如闭包)的情况下,将留在内存中不被销毁
3.2闭包
一个函数在执行完后一定会销毁吗?不一定。
function father(){ var tmp = 'hello' return function(){ console.log(tmp) } }
var result = father()
按照常理,这句话被执行完,father函数就结束了,意味着它里面的tmp变量也是应该被销毁的。如果这样看,里面的匿名函数是不能调用tmp的了。
但是。此时result这个变量,等价于什么?等价于匿名函数的定义,即可以写成如下形式:
var result = function(){ console.log(tmp) }
这样写和原来的father定义+father调用 看起来是等价的。
但是是等价的么?不是,因为tmp。去哪找tmp啊?没地方找。
所以,你看闭包这个概念被创建,是为了什么?为了能让函数A里面的函数B,能访问函数A里面的变量。而且不是时时调用的,是依赖调用的。
也就是说,函数B依赖于函数A存在,并且B是不着急调用的,但是B要有。这时候你想到了什么?
没错,对象!
function Person(name,age){ this.name=name this.age=age this.say = function(){ console.log(name+age) } }
这是一个构造函数的定义,构造函数的调用是:
var p=new Person('haha',20)
Person作为函数,没有返回值,那p到底是什么呢?很简单,p是通过构造函数Person创建的实例对象,也可以说成是:Person函数返回的是——以Person作为构造函数的 原型 对象。其实就是this.
那闭包的写法是什么?
function Person(name,age){ function pConstructor(){ var o = new Object() o.name=name o.age=age o.say=function(){ console.log(name+age) } o.constructor = Person return o } return pConstructor; }
这时候:
var pcons = Person('www',20)
这句话是把pcons指向pConstructor,而name和age都会被暂时储存起来,只有当我们调用pcons时才会创建新对象实例。
var pinstance = pcons()
这时候创建一个对象还需要两步,有没有方法让它变成一步呢?有,把原来的构造函数变成 匿名自调用函数 就可以了。
var Person = (function(){ function pConstructor(name,age){ var o = new Object() o.name=name o.age=age o.say=function(){ console.log(name+age) } o.constructor = Person return o } return pConstructor; })();
此时,我们创建一个对象实例
var p = Person('ww',12)
这就是闭包。闭包具体指的是什么?就是那个被包起来的函数。
也有的写法是返回一个对象,其实不只是返回一个对象,可以返回任何类型。
var Person = (function(){ function pConstructor(name,age){ var o = new Object() o.name=name o.age=age o.say=function(){ console.log(name+age) } o.constructor = Person return o } return { newObj:pConstructor } })(); var p = Person.newObj('eee',21)
闭包需注意几点:
1.闭包的形式:(fuinction(){})() 闭包内的函数只属于闭包。闭包中的变量也只有闭包内的函数可以访问。
2.闭包可以保存变量。
一般函数再执行完后,其作用域和变量就会被销毁;若函数内存在闭包,并且访问了其他函数的变量,那么在函数执行后变量不会被销毁,而是被闭包保存下来了,直到闭包被销毁(=null),才会销毁。
3.若闭包内的变量与全局变量冲突时,会调用闭包内的变量。
function A(){ function B(num){ var c = 10;//内部变量 c return num + c; } return B; } var c = 20;//外部变量c var result = A()(c); console.log(c);//20 console.log(result)//30
4.闭包只能取得包含函数中任何变量的最终值
5.闭包的this对象