8、面向对象编程
this
this,用于指代环境对象的,这里先讲解一下,方便之后理解。但是在不同情形下,this的指代是不同的:
1、this在一个对象的method出现,指代的是method所属的对象;
2、this在函数中出现,注意是普通函数,指代全局对象window;
3、this在事件中出现,指代事件绑定的对象;
4、this在函数中出现,在严格模式下,this 是未定义的(undefined);
以上就是this的指代了,特别交代一句,无论何时,匿名函数中的this全部指代全局window。
但是这里的this和接下来要讲的创建对象就不能一概而论,特殊情况特殊对待吧。
call apply bind
this的重定向
首先来说明一下这三个函数的作用,简单来讲就是:
把一个对象的方法,绑定给另外一个对象去使用。
call apply bind 相当于把this重新绑定了,直接替换了this
举个🌰:
var a = {
name = 'Leo';
age = 20;
getName() {
console.log(this.name+this.age);
}
}
var b = {
name = 'ruby';
age = 30;
}
a.getName(); // Leo, 20
a.getName.call(b); // ruby, 30 getName里面的this就绑定上b了,直接!!!
a.getName.apply(b);
a.getName.bind(b)();
注意,bind和上面的两个函数不太一样,bind返回的是一个函数,并不能自己去执行,需要加上()去执行它。
函数传参
var a = {
getName(name,age) {
console.log(name+age);
}
}
var b = {}
a.getName.call(b,'Lily',10); // Lily, 10
a.geyName.apply(b,['Lily',10]); // Lily 10 传参传递的是一个数组
a.getName.bind(b,'Lily',10)(); // Lily 10
call与apply实现bind
首先了解一下bind的特性,bind本身作用在一个函数上面,并且把函数的作用域this进行替换,替换成第一个参数的this,然后把新参数传进去:
首先了解一下重新定义this
Function.prototype.bind = function bind_new(obj_new){
var func = this; // 这是函数本来的this
return function(){
func.apply(obj_new); // 更换this
}
}
之后就是添加新参数了 这里需要讲一下,传递参数是有一个arguments的,这是实参。
Function.prototype.bind = function bind_new(obj_new){
var arg = arguments.slice(1); // 这是数组的原定参数
var func = this; // 这是函数本来的this
var bound = function(arg1){ // 所谓的函数柯里化
return func.apply(obj_new, [...arg, ...arg1]); // 更换this,并添加传递的参数
}
// 函数需要设置自己的原型对象
var fn = new Function() {}; // 搞一个父类
fn.prototype = func.prototype; // bind自己的原型应该是bind_new函数的原型
bound.prototype = new fn(); // 父类的实例是子类的原型
return bound;
}
创建对象
1、直接利用对象字面量{…}创建
var a = {
name: 'ruby',
age: 20,
getName(){
return this.name;
}
}
但是如果创建多个对象,就需要一遍一遍的写,希望能够批量创建对象。
2、Object构造函数
var Person = new Object(); // 直接继承自Object对象
Person.name = 'ruby'; // 因为对象的属性可以随时增加
Person.age = 20;
这样也是需要大量复写工作。
3、单例模式
单例模式,核心结构中值包含一个被称为单例的特殊类。一个类只有一个实例,即一个类只有一个对象实例
。
简单来讲,便是如果我们用 new 去创建实例,每次创建的都是同一个实例:
1、如果实例不存在,那么便需要利用构造函数创建一个实例
2、如果实例已经存在,那么就需要直接返回这个实例
看出来了,有点全局变量的意思,那么将全局变量转换成局部变量,就需要闭包,让我们来试一试。
首先,我们先来想一想构造函数,构造函数就是一个函数, 然后里面写一些属性和方法:
function fn(name){
this.name = name;
this.getName = function(){
console.log(this.name);
}
}
好了,有了构造函数,下面就需要放入闭包里面:
var CreateOne = (() => {
var instance = null; // 实例
return function(name){ // 利用了函数柯里化
if(instance != null){
return instance; // 实例存在就需要直接返回
}
instance = new fn(name); // 实例不存在,那么就创建一个并返回
return instance;
}
})(); // 函数让它执行
var instance1 = new CreateOne(‘ruby’); // ruby
var instance2 = new CreateOne(‘ruby2’); // ruby
4、使用工厂模式创建对象
通俗地讲,工厂模式就是将创建对象的语句放在一个函数里,通过传入参数来创建特定对象,最后返回创建的对象。这样子后面创建相同类型的对象可以直接调用函数,不用一遍遍复写。
function Person(name, age) {
var a = new Object();
a.name = name;
a.age = age;
a.opetion = funtion(){
console.log(this.name + this.age);
}
return a;
}
var ruby = Person('ruby', 20);
在Person函数中,返回的是一个对象,对象类型全是Object,这就很粗糙了。
5、构造函数
JavaScript不存在类的概念,那么稍微复杂的就是函数了。
构造函数的方法来创建对象:首先编写一个函数,在里面声明、定义局部变量以及内部函数,通过new 便可以将函数里面的this指向的是生成的对象。
function Person(name, age) {
this.name = name;
this.age = age;
this.opetion = funtion(){
console.log(this.name + this.age);
}
}
var ruby = new Person('ruby', 20);
利用了 new ,下面看一下new的操作:
1.创建一个新对象
2.将构造函数的作用域赋给新对象(因此this就指向了这个对象)
3.执行构造函数中的代码(为这个新对象添加属性)
4.返回新对象
然后就会发现,创建的实例的类型变成了Person,哦,有详细的类型了,完美。
对比工厂模式,我们可以发现以下区别:
1.没有显示地创建对象
2.直接将属性和方法赋给了this对象
3.没有return语句
4.终于可以识别的对象的类型。
用 new Person() 创建的对象会获得了一个constructor属性,它指向构造函数Person。
但是虽然没有复写的麻烦了,但是实际上每次创建对象,都会把Person函数跑一遍,里面每次运行到的属性和方法都只是属于实际对象的。举个🌰:
function Person(name, age) {
this.name = name;
this.age = age;
this.opetion = funtion(){
console.log(this.name + this.age);
}
}
var ruby = new Person('ruby', 20); // 构造函数帮助创建了两个属性和一个方法
var ruby2 = new Person('ruby2', 22); // 构造函数也帮忙创建了两个属性和一个方法
看出来了吧,因为this指向的是创建的对象,两次this分别指向ruby和ruby2,确实不用重写,也能找到对象类型,但是占了4个变量的空间和两个函数的空间,实际资源一点都没少占!!!!!!!
所以就有了最后一种方法。
6、原型创建
先来了解一下关于原型与原型链。
原型与原型链
1、原型
当我们创建一个函数时,函数会生成一个属性,prototype ,注意:只有函数才会有这个属性。然后这个 prototype 会指向这个函数的原型对象 Object,同时 prototype 下会生成一个 constructor 指针,这个指针会指向创建的函数,举个🌰:
function Hello(){
}
Hello.prototype; // Object
Hello.prototype.constroctor; // Hello()
上面构造函数创建对象的🌰:对象ruby是来自Person()这个构造函数,这个对象ruby就会生成两个属性 proto 和constructor , __proto__这个属性会指向构造函数Person的 prototype 指针指向的 Object;而 constructor 就指向Person()这个构造函数。
ruby.__proto__; // Object 原型对象
ruby.constructor; // Person() 构造函数
2、原型链
所以这种继承链接的基础就是对象的__proto__;
ruby对象的__proto__会找到构造函数的prototype,从而指向原型对象Object,然后原型对象会继续查看__proto__找自己的构造函数和原型对象,直到null,所以一连串的原型对象就构成了原型链。
ruby ————> Person.prototype(Object) ————> Object.prototype(Object的原型对象)————>null;
那么如何才能够减少空间的浪费呢,有一个办法,就是属性和方法不要写到函数里面,想办法写到函数的原型对象里面,没错,就是 Person.prototype里面,这样子每次使用都是同一个资源,还可以达到共享的效果。
function Person(){}
Person.prototype.name = 'Leo';
Person.prototype.age = 30;
Person.prototype.getName(){
return this.name;
}
var ruby = new Person(); // Leo,30 每次创建对象共享的同一份资源,内存省了
ruby.name = 'ruby';
ruby.age = 20; // ruby, 20
7、组合模式
所谓的组合模式,便是将构造函数创建对象实例和原型创建混合在一起使用:
function Person(name, age) {
this.name = name;
this.age = age;
}
} // 构造函数
Person.prototype.opetion = funtion(){
console.log(this.name + this.age);
} // 写在原型链上的函数,因此多个对象可以公用同一个函数
继承
创建完对象,来谈一谈关于继承吧,说到继承,看创建便知道JS是依靠原型创建,那么继承也是依靠原型,但是这里有必要说一下,原型继承需需要理解原型和原型链。
原型继承
所以原型继承的基础就是原型和原型链,核心是:将父类的实例作为子类的原型。
因为不存在类的概念,那么函数就是老大了,先弄一个所谓的父类:
function PersonOne(name) {
// 注意:这里的属性和方法都是给实例对象的
this.name = name;
this.opetion = funtion(){
console.log(this.name);
}
}
// 然后这个函数的原型对象就是 PersonOne.prototype
PersonOne.prototype.age = 10; // 给原型对象一个共享变量
function Person(name){
// 这是给Person的实例对象的属性
this.name = name;
this.opetion = funtion(){
console.log(this.name);
}
}
Person.prototype = new PersonOne(); // 父类实例(右)-- 子类原型(左)
// 然后就创建新对象了
var ruby = new Person('ruby'); // ruby,10
原型链:
ruby ————> Person.prototype ————> PersonOne.prototype
name,opetion,age ————> age
优点:
1、实现起来比较简单
2、父类所有的属性方法可以被所有实例共享
缺点:
1、父类想要增加属性和方法不容易,只能这样 PersonOne.prototype.XXX = YYY;
2、父类的属性方法可以被所有实例共享,也是一个缺点,因为相互影响
3、创建实例时,根本无法向PersonOne里面传递参数
4、继承链依赖于原型链,所以只能单一的继承原型对象的方法和属性
构造函数继承
其核心思想是:在子级构造函数中调用父级构造函数。
如何实现在一个构造函数中调用另一个函数?——call()和apply()
function PersonOne(age){
this.age = 20;
}
function Person(name, age){
PersonOne.call(this, age); // 注意,这个this是Person的实例,也就是把属性和方法绑定到Person上了
this.name = name;
}
var ruby = new Person('ruby', 20); // ruby, 20
var ruby2 = new Person('ruby2', 30); // ruby2, 30
因为压根就没有共享一个资源,所以ruby和ruby2彼此之间互不干涉,而且可以传递参数了,还有一个优点就是可以继承很多父类,只要调用call更改this就可以;但是问题也是那个问题,浪费资源,无法共享,而且只能拿去父类的构造函数里面的属性和方法,原型链上的拿不到。
组合继承
组合继承就是把构造函数继承+原型继承,把他们的优点结合起来。
function PersonOne(name){
this.name = name;
this.arr = [1,2,3]; // 这是实例属性,不会共享
}
PersonOne.prototype.age = 20; // 这是共享的属性
function Person(name){
PersonOne.call(this,name); // 拷贝一份PersonOne的实例属性 name和arr
this.getName(){
return this.name; // 新的实例方法
}
}
Person.prototype = new PersonOne(); // 原型链来一份
var ruby = new Person('ruby'); // getName,ruby,[1,2,3],20 其中20是共享的
class继承
上面的继承太复杂,不好理解,因此ES6推出了javascript的class继承,让类的定义更加简单,但其实实际上class只是一种另外一种原型继承的表达方式而已,它遵循的基础仍然是原型继承
。
下面是定义一个类,包括构造函数以及方法:
class Person{
// 构造函数
constructor(name,age){ // 构造函数
this.name = name;
this.age = age; // this 指向类的实例对象
}
// 下面是方法,但是注意,方法实际上是定义在Person 的原型对象上的
hello(){
console.log(this.name); // 方法里面的this也是指向实例对象的
}
}
看上面定义的Person,它其实是一种原型对象,然后constructor就是它的构造函数,类似于
function Person() { ... }
然后定义在原型对象的 hello 函数 也就是:
Person.prototype.hello = function ()
{
console.log('Hello!!!');
}
class继承可以直接通过extends来实现:
class primaryPerson extends Person {
constructor(name, age, grade){
super(name, age); // super是用来调用父类的构造函数的,可以直接传参,不用重写
this.grade = grade;
}
hey(){
console.log('hey');
}
}
var primaryPerson1 = new primaryPerson('ruby',20); // ruby,20,hello(),hey()
相对于上面的原型继承的写法,这种class继承方式的写法就简单很多。
本文来源廖雪峰老师教程的笔记,有疑惑可以直接访问廖雪峰老师教程:https://www.liaoxuefeng.com/wiki/1022910821149312/1023022043494624