前端学习之JavaScript入门——面向对象的程序设计

ECMA-262把对象定义为无序属性的集合,其属性可以包含基本值、对象或者函数。严格来说,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。
JavaScript这里的对象和Java语言中的类实例的对象不同,所以这里创建对象和Java等语言也不相同。

创建对象

正如Java一样子类继承于Object类一样,JavaScript的Object类型是所有它的实例的基础。JavaScript没有Java那种声明class一样的操作,也就是直接实例化Object,然后直接使用”Object实例.属性=属性值“的方式使之特例化为子类。

工厂函数

正因为如此,一开始创建对象可以使用工厂函数,工厂模式虽然可以创建多个相似对象,但是没有解决对象识别问题(即怎么知道一个对象的类型)。也就是虽然是同样的属性(属性值可能不同),但是确不是同一种类型。
前端学习之JavaScript入门——面向对象的程序设计_第1张图片

构造函数

引入构造函数来创建特定类型的对象,在对象中引入constructor(构造函数)属性,指向构造函数,用来标识对象类型。构造函数名使用大写首字母,为了区别于其他函数,因为构造函数本身也是函数,只不过可以用来创建对象而已。
前端学习之JavaScript入门——面向对象的程序设计_第2张图片
以上面的方式定义的构造函数是定义在Global对象(在浏览器中是window对象)中。

alert(person1.constructor == Person); // true
alert(person1 instanceof Person); // true
alert(person1 instanceof Object); // true

构造函数当作普通函数演示

// 当在全局作用域中调用一个函数时,this对象总是指向Global对象(在浏览器中是window对象)
Person("Greg",27);
window.sa();  // 我叫Greg
// 使用call或apply在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Greg",27);
o.sayName(); // 我叫Greg

缺点:实现需要分别实例化函数对象,浪费内存。alert(person1.say == person2.sayname) // false
前端学习之JavaScript入门——面向对象的程序设计_第3张图片
这里相当于function Person(n,a){ this.name = n; this.age = a; this.say = new Function(){console.log('我叫'+this.name);}}
解决重复实例函数的问题
前端学习之JavaScript入门——面向对象的程序设计_第4张图片
前端学习之JavaScript入门——面向对象的程序设计_第5张图片

原型

每个函数都有一个prototype(原型)属性,这个属性是一个指针,指向一个原型对象,该对象用来存放由特定类型的所有实例共享的属性和方法。
前端学习之JavaScript入门——面向对象的程序设计_第6张图片
在这里插入图片描述
这里是使用构造函数和原型对象混合定义方式,将属性定义在构造函数中,将方法定义在原型对象中。
前端学习之JavaScript入门——面向对象的程序设计_第7张图片
使用原型对象的好处是可以让所有对象实例共享它所有包含的属性和方法。无论什么时候,只要创建新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。在默认情况下,原型对象都会自动获得一个constructor构造函数属性,这个属性是一个指向prototype属性所在函数的指针
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性[[Prototype]]),指向构造函数的原型对象。这个连接存在于实例与构造函数的原型对象之间,而不是存在于实例与构造函数之间。
前端学习之JavaScript入门——面向对象的程序设计_第8张图片

两种方法访问[[prototype]]关系:

alert(Person.prototype.isPrototypeOf(Person1));  // true isPrototypeOf是原型对象的方法,判别Person实例变量的[[prototype]]是否指向该原型对象
alert(Object.getPrototypeOf(Person1)==Person.prototype); //true Person实例变量的[[prototype]]是否指向该原型对象

前端学习之JavaScript入门——面向对象的程序设计_第9张图片
前端学习之JavaScript入门——面向对象的程序设计_第10张图片
通过实例对象.constructor也可以取到原型对象中constructor所指构造函数,弥合了上一节中构造函数方式创建对象引入的“引入构造函数来创建特定类型的对象,在对象中引入constructor(构造函数)属性,指向构造函数,用来标识对象类型”的设计。
前端学习之JavaScript入门——面向对象的程序设计_第11张图片

特殊的原型语法

前端学习之JavaScript入门——面向对象的程序设计_第12张图片
这里将Person.prototype设置为等于一个以对象字面量形式创建的新prototype对象。
每创建一个函数,就同时创建它的prototype对象,而这个对象会自动获得constructor属性,并指向该函数。但是这里使用对象字面量形式创建的新prototype对象,本质上完全重写默认的prototype对象(这里更正一下,原有prototype对象并没有消失,这里只是切断了构造函数和原有prototype对象的联系),因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。也就是说Person构造函数的prototype属性指向新的prototype对象,而这个prototype对象的constructor属性指向Object构造函数。在之后new创建的实例的[[prototype]]属性同样也是指向新的prototype对象,并且constructor属性和新prototype对象的constructor属性相同,指向Object构造函数。

前端学习之JavaScript入门——面向对象的程序设计_第13张图片
前端学习之JavaScript入门——面向对象的程序设计_第14张图片
以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true。默认情况下原生的constructor属性是不可枚举的。

尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果重写整个原型对象呢?调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另一个对象就等于切断了构造函数与最初原型之间的联系。

function Person(){}
var friend = new Person();
Person.prototype = {
	constructor:Person,
	name:"Nicholas";
	age:29;
	job:"Software Engineer";
	sayName:function(){ alert(this.name); };
};
friend.sayName(); //error

这里创建了一个friend实例对象,但是该实例对象的[[Prototype]]指针指向为改写前的prototype对象,后面使用字面量改写后,friend实例对象的[[Prototype]]指针并没有被改写,依旧指向改写前的prototype对象,而改写前的prototype对象的constructor属性依旧指向Person构造函数。该写后的新prototype对象替代了旧prototype对象的位置,Person构造函数的prototype属性指向新prototype对象,新prototype对象的constructor属性指向Person构造函数。

单纯的原型模式,使用prototype对象对实例对象所共有的属性和方法进行了共享,这种共享会导致难以在new新对象时为各个对象分配相同属性名但是拥有各自独特数据的属性。使用构造函数和原型混合的模式可以解决这个问题。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。

function Person(name,age,job){
	this.name = name;
	this.age = age;
	this.job = job;
	this.friends = ["Shelby","Court"];
}
Person.prototype = {
	constructor:Person,
	sayName:function(){ alert(this.name); };
};
var person1 = new Person("Nicholas",29,"Software Engineer");
var person2 = new Person("Greg",27,"Doctor");
person1.frineds.push("Van");
alert(person1.friends); // "Shelby,Court,Van"
alert(person2.friends); // "Shelby,Court"
alert(person1.friends === person2.friends);  // false 实例属性非共享
alert(person1.sayName === person2.sayName);  // true

在实例中添加属性

不能通过对象实例重写原型中的值,如果在实例中添加属性,而该属性与实例原型中的一个属性同名,那就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。即使将实例中新创建的属性设置为null,也只会在实例中设置这个属性。不过,使用delete操作符则可以完全删除实例属性,从而可重新访问原型中的属性。

使用从Object继承来的hasOwnProperty方法可以检测一个属性是存在于实例中,还是存在于原型中。只在给定属性存在于对象实例中时,才会返回true。

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();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); // false

Person1.name = "Greg";
alert(person1.hasOwnProperty("name")); // true
alert(person2.hasOwnProperty("name")); // false

delete person1.name;
alert(person1.hasOwnProperty("name")); // false

in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。在使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为false的属性)的实例属性也会在for-in循环中返回。

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();
var person2 = new Person();

alert(person1.hasOwnProperty("name")); // false
alert("name" in person1); // true

person1.name = "Greg";
alert(person1.hasOwnProperty("name")); // true
alert("name" in person1); // true

alert(person2.hasOwnProperty("name")); // false
alert("name" in person1); // true

delete person1.name;
alert(person1.hasOwnProperty("name")); // false
alert("name" in person1); // true

可以使用Object.keys方法取得对象上所有可枚举的实例属性,也可以通过Object.getOwnPropertyNames方法得到所有实例属性,无论它是否枚举。

var keys = Object.keys(Person.prototype);
alert(key); // "name,age,job,sayName"

var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1key = Object.keys(p1);
alert(p1key); // "name,age"

var keys = Object.getOwnPropertyNames(Person.prototype);
alert(keys); // "constructor,name,age,job,sayName"

继承

原型链

基本思想是利用原型让一个引用类型继承另一个引用类型的属性和方法。也就是在prototype原型中还有指向父类prototype原型的[[prototype]]属性
在这里插入图片描述

简单回顾一下构造函数、原型和实例的关系:每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的职责,而实例都包含一个指向原型对象的内部指针。那么,假如我们将原型对象上替换为另一个类型的实例。显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条。这就是所谓原型链的基本概念。
前端学习之JavaScript入门——面向对象的程序设计_第15张图片
前端学习之JavaScript入门——面向对象的程序设计_第16张图片

function SuperType(){ this.property = true; }  // 实例属性,在SuperType实例中存储
SuperType.prototype.getSuperValue = function(){ return this.property; }; //方法,在SuperType原型对象中存储
function SubType(){ this.subproperty = false; } // 实例属性,在SubType实例中存储
//继承SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){ return this.subproperty; }; //方法,在SubType原型对象中存储
var instance = new SubType();
alert(instance.getSuperValue); //true

instance指向SubType的原型,SubType的原型又指向SuperType的原型。instance.constructor现在指向的是SuperType。SubType的原型指向了另一个对象-SuperType的原型,而这个原型对象的constructor属性指向的是SuperType。
调用instance.getSuperValue会经历三个搜索步骤:搜索实例->搜索SubType.prototype-> 搜索SuperType.prototype,最后一步才会找到该方法。

所有引用类型默认都继承了Object,而这个继承也是通过原型链实现的。所有函数的默认原型都是Object的实例,因此默认原型都会包含一个内部指针,指向Object.prototype。上面例子的SuperType的原型对象[[Prototype]]属性指向Object的原型对象。

如果子类型需要覆盖超类中的某个方法,或者需要添加超类中不存在的某个方法,新添加的代码一定要放在替换原型语句之后。通过原型链实现继承时,不能使用对象字面量创建原型方法。

确定原型和实例关系(instanceof),确定原型链所派生的实例的原型(isPrototypeOf)

alert(instance instanceof Object);  //true
alert(instance instanceof SuperType);  //true
alert(instance instanceof SubType);  //true
alert(Object.prototype.isPrototypeOf(instance));
alert(SuperType.prototype.isPrototypeOf(instance));
alert(SubType.prototype.isPrototypeOf(instance));

前端学习之JavaScript入门——面向对象的程序设计_第17张图片
前端学习之JavaScript入门——面向对象的程序设计_第18张图片
在这里插入图片描述

原型链的问题

第一个问题:由于原型链继承,子类会将父类的实例替换到子类的原型对象中,那么父类的实例属性成为了子类的原型属性。
第二个问题:在创建子类型的实例时,不能向超类型的构造函数中传递参数。实际上,是说没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数。

借用构造函数-经典继承

在解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数的技术,即在子类型构造函数的内部调用超类型构造函数。
前端学习之JavaScript入门——面向对象的程序设计_第19张图片

function SuperType(){ this.colors = ["red","blue", "green"]; }
function SubType(){ 
	//继承SuperType
	SuperType.call(this);
	//调用apply或call方法在新创建的对象上执行构造函数
}
var instance1 = new SubType();
instance1.colors.push("black");
alert(instance1.colors); // "red,blue, green,black"
var instance2 = new SubType();
alert(instance2.colors); // "red,blue, green"

在新创建的SubType实例的环境下调用SuperType构造函数。这回在新SubType对象上执行SuperType()函数中定义的所有对象初始化代码。也就是SubType的每个实例就都会具有自己的colors属性的副本。也就解决第一个问题了。
并且可以在子类型构造函数中向超类型构造函数传递参数。

function SuperType(name){ this.name = name; }
function SubType(){ 
	//继承SuperType,同时传递参数
	SuperType.call(this,"Nicholas");
	//调用apply或call方法在新创建的对象上执行构造函数
	this.age = 29;
}
var instance = new SubType();
alert(instance1.name); // "Nicholas"

组合继承:原型链+借用构造函数

使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。

function SuperType(name){
	this.name = name;
	this.color = ["red","blue","green"];
}
SuperType.prototype.sayName= function(){alert(this.name);};
function SubType(name,age){
//继承实例属性
	SuperType.call(this,name);
	this.age = age;
}
//继承原型属性和方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;  //改变构造函数属性为SubType的构造函数
SubType.prototype.sayAge = function(){alert(this.age);};

var instance1 = new SubType("Nicholas",29);
instance1.colors.push("black");
alert(instance1.colors); //"red,blue,green,black"
instance1.sayName(); //"Nicholas"
instance1.sayAge(); //29

var instance2 = new SubType("Greg",27);
alert(instance1.colors); //"red,blue,green"
instance1.sayName(); //"Greg"
instance1.sayAge(); //27

前端学习之JavaScript入门——面向对象的程序设计_第20张图片

原型式继承

在object函数内部先创建一个临时性的构造函数,将该构造函数的原型替换为传入的对象o,最后返回临时类型的一个新实例。object函数对传入其中的对象执行了一次钱复制。

function object(o){
	function F(){}
	F.prototype = o;
	return new F();
}

前端学习之JavaScript入门——面向对象的程序设计_第21张图片
从log可以看出Object字面量的定义相当于new Object()定义。这里的person对象是一个Object的实例,其中有一个__proto__属性指向Object的原型,而name和friend是person变量的属性。
前端学习之JavaScript入门——面向对象的程序设计_第22张图片
从log中看出person实例替换为了anotherPerson的原型对象,从anotherPerson的原型对象中可以看到person的name和共享的friends属性。从父类子类的属性的打印情况可以看出上述情况。
前端学习之JavaScript入门——面向对象的程序设计_第23张图片
同样yetAnotherPerson和上面的anotherPerson类似。从中可以看出,使用原型式继承,父类中的数值对象被共享,而基本类型被设置为子类实例的各自的数据了,但是在子类的原型对象中同样有父类实例的数据。
这种原型式继承,要求必须有一个对象可以作为另一个对象的基础。在这个例子中,作为基础的是person对象,传入object函数后,返回新对象,这个对象将person作为原型,所以它的原型中包含一个基本类型属性和引用类型属性。这意味着person.friends不仅属于person所有,而且会被其他新建对象共享。
ECMAScript 5通过新增Object.create方法规范了原型式继承,接收两个参数:作为新对象原型的对象和一个为新对象定义额外属性的对象。
前端学习之JavaScript入门——面向对象的程序设计_第24张图片
前端学习之JavaScript入门——面向对象的程序设计_第25张图片
前端学习之JavaScript入门——面向对象的程序设计_第26张图片

寄生式继承

寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,返回对象。

function createAnother(original){
	var clone = object(original); //调用函数创建一个新对象
	clone.sayHi = function(){alert("hi");}; //以某种方式增强对象
	return clone;   //返回这个对象
}

寄生组合式继承

组合继承:原型链+借用构造函数 这种方法实例化时需要调用两次父类的构造函数。
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。不必为了指定子类的原型而调用超类型的构造函数,仅仅需要复制超类原型而已。使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。

function inheritPrototype(subType, superType){
	var prototype = Object(superType.prototpye); //创建对象
	prototype.constructor = subType; //增强对象
	subType.prototype = prototype; //指定对象
}

function SuperType(name){
	this.name = name;
	this.color = ["red","blue","green"];
}
SuperType.prototype.sayName= function(){alert(this.name);};
function SubType(name,age){
//继承实例属性
	SuperType.call(this,name);
	this.age = age;
}
inheritPrototype(subType, superType);
SubType.prototype.sayAge = function(){alert(this.age);};

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式就是在一个函数内部创建另一个函数。

function createComparisonFunction(propertyName){
	return function(object1, object2){
		var value1 = object1[propertyName];
		var value2 = object2[propertyName];
		if(value1<value2){
			return -1;
		}else if(value1>value2){
			return 1;
		}else{ return 0; }
	}
}

当某个函数被调用时,会创建一个执行环境(execution context)及相关的作用域链。在作用域链中,外部函数的活动对象始终处于第二位,外部函数的外部函数的活动对象处于第三位…直到作为作用域终点的全局执行环境。后台的每个执行环境都有一个表示变量的对象-变量对象。全局环境的变量对象始终存在。在创建函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内存的[[Scope]]属性中。当调用函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。此后,不断有活动对象被创建并推入执行环境作用域链的前端。作用域链本质上是一个指向变量对象的指针列表,它只引用但不实例包含变量对象。
在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。因此在createComparisonFunction函数内部定义的匿名函数的作用域中,实际上将会包含外部函数createComparisonFunction的活动对象。在匿名函数从createComparisonFunction函数中被返回后,它的作用域被初始化为包含createComparisonFunction函数的活动对象和全局变量对象。这样,匿名函数就可以访问在createComparisonFunction函数中定义的所有变量。更重要的是,createComparisonFunction函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域仍然在引用这个活动对象。换句话说,当createComparisonFunction函数返回后,其执行环境的作用域会被销毁,但它的活动对象仍然会留在内存中;直到匿名函数被销毁后,createComparisonFunction函数活动对象才会被销毁。
前端学习之JavaScript入门——面向对象的程序设计_第27张图片
看函数定义在哪里,哪里的活动对象被创建并推入执行环境作用域链的前端
在这里插入图片描述
前端学习之JavaScript入门——面向对象的程序设计_第28张图片
前端学习之JavaScript入门——面向对象的程序设计_第29张图片

这种方式定义的函数也是闭包的一种,因为点击后也会执行onclick指定的匿名函数。但是匿名函数引用的变量都是同一个,也就是i变量,在for中执行完就变为了5。所以不管点那个都输出第5个标签。
在这里插入图片描述
前端学习之JavaScript入门——面向对象的程序设计_第30张图片
使用闭包来模仿块级作用域,将i变量的值引入模仿块级作用域中。将i作为闭包函数的实参,并赋值给形参m,这个形参处于匿名函数自己的作用域中。调用onclick指定的匿名函数时,该匿名函数的作用域链中第二层就是该形参m。
前端学习之JavaScript入门——面向对象的程序设计_第31张图片

关于this对象

前端学习之JavaScript入门——面向对象的程序设计_第32张图片
前端学习之JavaScript入门——面向对象的程序设计_第33张图片
前端学习之JavaScript入门——面向对象的程序设计_第34张图片
前端学习之JavaScript入门——面向对象的程序设计_第35张图片
前端学习之JavaScript入门——面向对象的程序设计_第36张图片

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
前端学习之JavaScript入门——面向对象的程序设计_第37张图片
前端学习之JavaScript入门——面向对象的程序设计_第38张图片

在这里插入图片描述
前端学习之JavaScript入门——面向对象的程序设计_第39张图片
前端学习之JavaScript入门——面向对象的程序设计_第40张图片

前端学习之JavaScript入门——面向对象的程序设计_第41张图片
在这里插入图片描述
前端学习之JavaScript入门——面向对象的程序设计_第42张图片

前端学习之JavaScript入门——面向对象的程序设计_第43张图片
前端学习之JavaScript入门——面向对象的程序设计_第44张图片

参考:
传智播客携手黑马共同带你从入门JavaScript到入狱JavaScript

你可能感兴趣的:(网络前端)