js 面向对象相关

270 阅读16分钟

javaStript 对象入门

背景介绍

在 JavaScript 中,大多数事物都是对象, 从作为核心功能的字符串和数组,到建立在 JavaScript 之上的浏览器API。你甚至可以自己创建对象,将相关的函数和变量封装打包成便捷的数据容器。理解这种面向对象(object-oriented, OO) 的特性对于进一步学习 JavaScript语言知识是必不可少的。这个模块将帮助你了解“对象”,先详细介绍对象的设计思想和语法,再说明如何创建对象

所谓面向对象编程,主要有以下几个知识点

一、理解对象以及访问对象的两种表示法

二、'this'的含义

三、面向对象的程序设计

四、构造函数和对象实例

五、原型链 prototype constructor

六、继承

那什么是对象呢

理解对象的几个基础概念:

1、类:定义对象的特征。它是对象的属性和方法的模板定义。(类是对象的抽象)

2、对象(或称实例):类的一个实例。(对象是类的具象)

3、属性:对象的特征,比如颜色、尺寸等。

4、方法:对象的行为,比如行走、说话等。

5、构造函数:对象初始化的瞬间被调用的方法。

注意

1.构造函数始终都应该以一个大写字母开头。

2.要创建Person的新实例,必须使用 new 操作符。

一 、理解对象以及访问对象的两种表示法 括号语法和点语法

对象是一个包含相关数据和方法的集合(通常由一些变量和函数组成,我们称之为对象里面的属性和方法)。我们来看一个例子

var person = {
    name: ['Bob', 'Smith'],
    age: 32,
    gender: 'male',
    interests: ['music', 'skiing'],
    bio: function () {
        alert(this.name[0] + ' ' + this.name[1]+ ' is ' + this.age + ' years old. He likes ' + this.interests[0] + ' and ' + this.interests[1]+ '.');
    },
    geets: function () {
        alert('Hi! I\'m ' + this.name[0]+ '.');
    }
};

这就是一个典型的对象的例子,作为一个人,具有姓名,年龄,性别,爱好等等属性,而且有各种方法,会打招呼(greeting()),会做饭。

而我们想要访问到这个属性的话,就要用到点表示法,person.age.调用方法,也是同样,person.greeting();即可

我们来看一下括号表示法和点表示法的区别

function append() {
    var attr=$("#name").val();
    var num=$("#value").val();
   person[attr]=num;
    console.log(person);
}

我们通过括号表示法,可以给对象添加新的key和value,

图片alt

继续添加

图片alt

二、this的指向

在对象里,关键字"this"指向了当前代码运行时的对象,比如上面这个代码,this实际上指向了当前的对象。所以当我们调用这个方法的时候,geets()

geets: function (){
    alert('Hi! I\'m ' + this.name[0] + '.');
}

会弹出当前对象的名字

改变this指向的三个方法 call() apply() bind()

1、call()

()可传多个参数 第一个参数:this指向 其他如果要传参,后面依次是参数,例:
var o = {
	name: 'lisi'
}
 function fn(a, b) {
      console.log(this);
      console.log(a+b)
};
fn()// 此时的this指向的是window
fn.call(o,1,2)//此时的this指向的是对象o,参数使用逗号隔开

2、apply()

()可传两个参数 第一个参数:this指向 第二个参数用数组的形式表示参数 例:

// 经常用域数组中
var o = {
	name: 'lisi'
}
 function fn(a, b) {
      console.log(this);
      console.log(a+b)
};
fn()// 此时的this指向的是window
fn.apply(o,[1,2])//此时的this指向的是对象o,参数使用数组传递

3、bind()

bind 只改变this指向,不会调用函数执行

// 如果只是想改变 this 指向,并且不想调用这个函数的时候,可以使用bind
var o = {
 name: 'lisi'
 };

function fn(a, b) {
	console.log(this);
	console.log(a + b);
};
var f = fn.bind(o, 1, 2); //此处的f是bind返回的新函数
f();//调用新函数  this指向的是对象o 参数使用逗号隔开

三个方法的相同和不相同处

相同处:

这三个方法都可以改变函数this的指向

不同处:

1、call 和 apply 会调用函数, 并且改变函数内部this指向.
2、call 和 apply传递的参数不一样,call传递参数使用逗号隔开,apply使用数组传递
3、bind 不会调用函数, 可以改变函数内部this指向.

应用场景:

1、call()方法用于继承
2、apply()方法用于数组
3、bind()方法用于想要改变this指向但又不想调用函数的时候。

三 、面向对象的程序设计

最基本的 OOP 思想就是我们想要在我们的程序中使用对象来表示现实世界模型,并提供一个简单的方式来访问它的功能,否则很难甚至不能实现.

对象可以包含相关的数据和代码,这些代表现实世界模型的一些信息或者功能,或者它特有的一些行为.对于一个人(person)来说,我们能在他们身上获取到很多信息(他们的住址,身高,鞋码,基因图谱,护照信息,显著的性格特征等等),然而,我们仅仅需要他们的名字,年龄,性别,兴趣这些信息,然后,我们会基于他们的这些信息写一个简短的介绍关于他们自己,在最后我们还需要教会他们打招呼。以上的方式被称为抽象-为了我们编程的目标而利用事物的一些重要特性去把复杂的事物简单化

比如对于人这个类来说,它具有各种基本属性和方法,人与人之间的区别只是属性的值不一样,这样我们就简单构造了一个类来描述真实的世界。

对于前端来说,我们在javaScript里用构造函数来实现面向对象编程。来看一个例子!

function Person(first, last, age, gender,interests) {
    this.name = {
        first,
        last
    };
    this.age = age;
    this.gender = gender;
    this.interests = interests;
}

四、原型链

JavaScript 常被描述为一种基于原型的语言 (prototype-basedlanguage)——每个对象拥有一个原型对象,对象以其原型为模板、从原型继承方法和属性。原型对象也可能拥有原型,并从中继承方法和属性,一层一层、以此类推。这种关系常被称为原型链 (prototypechain),它解释了为何一个对象会拥有定义在其他对象中的属性和方法。

比如当我们调用person1.valueOf方法的时候,先是在person1里查找valueOf()方法,没找到,继续找它的原型,也就是构造函数Person,还是没找到,接着去对象的方法里去找,找到了,然后就成功调用了。

constructor和prototype属性

prototype 属性:继承成员被定义的地方,继承的属性和方法是定义在 prototype 属性之上的。然后每个对象实例都具有 constructor 属性,它指向创建该实例的构造器函数。

每一个构造函数都有一个原型对象(Person.prototype);原型对象都包含指向构造函数的指针(constructor);每个实例都包含指向原型对象的指针(看不见的_proto_指针)

某个构造函数的原型对象是另一个构造函数的实例;这个构造函数的原型对象就会有个(看不见的_proto_指针)指向另一个构造函数的原型对象;

那么另一个原型对象又是其他的构造函数实例又会怎么样,就这样层层递进,形成原型链;来具体看一下吧

//第一个构造函数;有一个属性和一个原型方法
		function SuperType(){
    			this.property=true;
		} 
 
		SuperType.prototype.getSuperValue=function(){
    			return this.property
		}
 
 
		//第二个构造函数;目前有一个属性
			function SubType(){
 			   this.subproperty=false
		}
 
		//继承了SuperType;SubType原型成了SuperType的实例;实际就是重写SubType的原型对象;给SuperType原型对象继承了
			SubType.prototype=new SuperType()
 
		//现在这个构造函数有两个属性(一个本身的subproperty,一个继承的存在原型对象的property);两个方法(一个原型对象的getSubValue,一个原型对象的原型对象的getSuperValue)
			SubType.prototype.getSubValue=function(){
    			return this.subproperty
		}
 
			var instance=new SubType()  //创建第二个构造函数的实例
 
		console.log(instance.getSuperValue())  //true 先查找instance这个实例有没有此方法;显然没有,再查找SubType原型对象有没有此方法;也没有,再查找SubType原型对象的原型对象;显然是存在的

注意:

instance的constructor现在指向的是SuperType这个构造函数;

因为原来的SubType.prototype被重写了,

其内部的constructor也就随着SubType.prototype的原型对象的constructor指向构造函数SuperType

完整的原型

在原型那节已经提了些,还是再说一下。完整的原型包括Object。

所有函数的默认原型都是Object的实例;每个默认原型都有个—_proto_指针指向Object.prototype;因此自定义类型都继承如toString,valueOf的方法

而Object.prototype的_proto_指针指向null来结束原型链。以Person构造函数为例,看看完整的原型链图

原型和实例的关系判断

第一种使用instanceof操作符: 测试实例和原型链中出现的构造函数,结果为true

第二种使用isPrototypeOf()方法: 只要是原型链中出现过的原型,都可以说是该原型链所派生的实例的原型

	console.log(instance instanceof Object)   //都为true
	console.log(instance instanceof SuperType)
	console.log(instance instanceof SubType)
    
	console.log(Object.prototype.isPrototypeOf(instance)) //都为true
	console.log(SuperType.prototype.isPrototypeOf(instance))
	console.log(SubType.prototype.isPrototypeOf(instance))

谨慎定义方法

注意:给原型对象添加方法,一定放在替换原型的后面,因为放在替换原型之前是找不到了,原型会被重写的;

注意:在通过原型链继承时,不能使用对象字面量创建原型方法,因为也会重写原型链;

function SuperType(){
 this.property=true;
	} 
 
	SuperType.prototype.getSuperValue=function(){
    		return this.property
	}
 
	function SubType(){
    		this.subproperty=false
	}
 
	//继承SuperType
	SubType.prototype=new SuperType()
    //继承SuperType
	SubType.prototype=new SuperType()
 
	//使用字面量添加新方法,导致上一行无效   因为现在的原型替换了Object实例而非SuperType的实例,关系中断
	SubType.prototype={
  		 getSubValue:function(){
      		 return this.subproperty;
 	  },
  	 somOtherMethod:function(){
      	return false
   		}
	};
 
	var instance=new SubType()
	console.log(instance.getSuperValue())  //error

原型链的问题

包含引用类型值的原型:当实例是另一函数的原型时,引用类型值就会变成原型上的属性,就会被另一函数的实例所共享。

function SuperType(){
   		this.colors=["yellow","red","olive"]
	}
 
	function SubType(){
	}
 
	SubType.prototype=new SuperType()  //color实际上就是原型上的了
 
	var instance1=new SubType()
	instance1.colors.push("purple")
	var instance2=new SubType()
 
	console.log(instance1.colors==instance2.colors)  //true

创建子类型实例时,不能向超类型的构造函数传递参数(没有办法在不影响所有对象实例的情况下,给超类型的构造函数传递参数)

借助构造函数

为了解决原型中包含引用类型值带来的问题,利用构造函数来解决

在子类型构造函数的内部调用超类型构造函数(函数是特定环境中执行代码的对象,可以通过apply或call调用)

function SuperType(){
   		 this.color=["yellow","red","olive"]
	}
 
	function SubType(){
   		 //继承了SuperType
   		SuperType.call(this)
	}
 
	var instance1=new SubType()
	instance1.color.push("purple")
	var instance2=new SubType()
 
	console.log(instance1.color)  //["yellow","red","olive","purple"]
	console.log(instance2.color)  //["yellow","red","olive"]
 
 
	//传递参数
	function SuperType(name){
  		 this.name=name
	}
	function SubType(){
    		SuperType.call(this,"double")
   		this.age=12
	}
 
	var instance1=new SubType()
	console.log(instance1.name)  //double
	console.log(instance1.age)  //12

问题:仅仅借鉴构造函数,那么避免不了构造函数的问题,方法都在构造函数定义了,函数无法复用

javaScript 原型链之原型对象、实例和构造函数三者之间的关系

五、继承

1、原型链式继承

核心: 将父类的实例作为子类的原型

将构造函数的原型设置为另一个构造函数的实例对象,这样就可以继承另一个原型对象的所有属性和方法,可以继续往上,最终形成原型链

父类

// 定义一个动物类
function Animal (name) {
  // 属性
  this.name = name || 'Animal';
  // 实例方法
  this.sleep = function(){
    console.log(this.name + '正在睡觉!');
  }
}
// 原型方法
Animal.prototype.eat = function(food) {
  console.log(this.name + '正在吃:' + food);
};

子类继承

function Cat(){ 
}
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';

// Test Code
var cat = new Cat();
console.log(cat.name);
console.log(cat.eat('fish'));
console.log(cat.sleep());
console.log(cat instanceof Animal); //true 
console.log(cat instanceof Cat); //true

问题

a、来自原型对象的所有属性被所有实例共享
b、创建子类实例时,无法向父类构造函数传参

2、构造函数继承

使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function SuperType() {
    this.colors = ["red", "blue", "green"];
}

function SubType() {
    //继承SuperType
    SuperType.call(this);
}

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" 

特点:

解决了1(原型式继承)中,子类实例共享父类引用属性的问题

创建子类实例时,可以向父类传递参数
可以实现多继承(call多个父类对象)
缺点:
实例并不是父类的实例,只是子类的实例
只能继承父类的实例属性和方法,不能继承原型属性/方法
无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

3、组合式继承

将原型链和借用构造函数的技术组合到一块。使用原型链实现对原型属性和方法的继承,而通过构造函数来实现对实例属性的继承。

function SuperType(name) {
    this.name = name;
    this.colors = ["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.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(instance2.colors); //"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge(); //27 

将SubType的原型指定为SuperType的一个实例,大致步骤和原型链继承类似,只是多了在SubType中借调SuperType的过程。实例属性定义在构造函数中,而方法则定义在构造函数的新原型中,同时将新原型的constructor指向构造函数。可以通过instanceof和isPrototypeOf()来识别基于组合继承创建的对象。避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为JS中最常用的继承模式

4、原型式继承

不自定义类型的情况下,临时创建一个构造函数,借助已有的对象作为临时构造函数的原型,然后在此基础实例化对象,并返回

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

本质上是object()对传入其中的对象执行了一次浅复制

var person = {
 name: "Nicholas",
 friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends); //"Shelby,Court,Van,Rob,Barbie" 

原型的引用类型属性会在各实例之间共享。

当只想单纯地让一个对象与另一个对象保持类似的情况下,原型式继承是完全可以胜任的

5、寄生组合式继承

组合继承是JS中最常用的继承模式,但其实它也有不足,组合继承无论什么情况下都会调用两次超类型的构造函数,并且创建的每个实例中都要屏蔽超类型对象的所有实例属性。

寄生组合式继承就解决了上述问题,被认为是最理想的继承范式。

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

function inheritPrototype(superType, subType) {
    var prototype = object(superType.prototype);
    prototype.constructor = subType;
    subType.prototype = prototype;
}

function SuperType(name) {
    this.name = name;
    this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
    alert(this.name);
};

function SubType(name, age) {
    SuperType.call(this, name);
    this.age = age;
}

inheritPrototype(SuperType, SubType);   // 这一句,替代了组合继承中的SubType.prototype = new SuperType()

不必为了指定子类型的原型而调用超类型的构造函数,我们需要的只不过是超类型原型的一个副本在inheritPrototype()函数中所做的事:

1.在inheritPrototype函数中用到了原型式继承中的object()方法,将超类型的原型指定为一个临时的空构造函数的原型,并返回构造函数的实例。
2.此时由于构造函数内部为空(不像SuperType里面有实例属性),所以返回的实例也不会自带实例属性,这很重要!因为后面用它作为SubType的原型时,就不会产生无用的原型属性了,借调构造函数也就不用进行所谓的“重写”了。
3.然后为这个对象重新指定constructor为SubType,并将其赋值给SubType的原型。这样,就达到了将超类型构造函数的实例作为子类型原型的目的,同时没有一些从SuperType继承过来的无用原型属性。

讨论题

一、原型对象和构造函数的区别

  • 1、每个构造函数都有一个指向原型对象的指针 prototype。
  • 2、原型对象上有指向构造函数的指针 constructor。
  • 3、实例对象包含着指向原型对象的内部指针_proto_。

二、什么是类?

  • 类(class)这个概念来源于OOP(Object Oriented Programming),也就是面向对象编程,OOP是一种计算机编程架构,其有着封装,继承,多态三种特性。
  • 而类在OOP中是实现信息封装的基础。
  • 类是一种用户定义类型,也称类类型。
  • 每个类包含数据说明和一组操作数据或传递消息的函数。
  • 类的实例称为对象。
  • 在ES5之前,JS中要表达一个类,要用一种叫做prototype-based的语法风格。
  • 在ES6中,引入的了class关键字

三、除了面向对象编程,还有哪些编程方法?

  • 1.命名空间 同其它高级语言一样,js中的命名空间概念,也是为了降低命名冲突,但js没有命名空间keyword。js实现命名空间的思路是定义一个全局变量,将此命名空间的变量和方法,定义为这个全局变量的属性
  • 2.初始化分支和延迟定义模式 这两个模式不同之处,能够从js框架设计角度考虑。构造一个框架,有些模块必须初始化的,比方jquery的$符号,另外一些仅仅有被调用到才须要初始化操作。这种优点在于,保证了框架的可用性和载入效率上的最优化。
  • 3.配置对象的模式 配置对象的模式,用于处理函数中有非常多个參数和方法的问题。用对象来替代多个參数,即让这些參数都成为对象某一属性。优势在于:不用考虑參数的顺序、跳过某些參数设置、扩展性和可读性更强。
  • 4.私有函数公有化与自运行函数模式 对象中私有函数对外不可见,私有函数公有化模式,用到了自运行函数的模式,返回一个对象,保有对自由函数可訪问性。
  • 5.链式调用模式 链式调用模式,能够在单行中调用多个方法,就好像他们被链接在一起。思路是:在方法中返回this指针,这样就实现了链式调用。