如何理解js中的面向对象

众所周知,由于js语言的特性,弱类型解释性语言,其本身并不具备类的特性,而又由于其独特的原型链的数据结构,使其可以通过一些特殊的方法来实现类的定义,甚至于实现面向对象的三大特性-继承,封装和多态

面向过程和面向对象代表的是两种思维方式,无所谓好坏之分。面向过程是指根据问题写出对应的解决方法从而达到目的,而面向对象则是从总体的角度去出发,将一类问题的解决方法归并在一起,而其中类的概念就是指具有某以相同特征的事物的抽象,即将很多相同或是相似的面向过程组合在一起。

类的封装

let Father = function (name, age){
    this.name = name;
    this.age = age;
}
Father.propotype.getName = function() {
    console.log(this.name)
}

复制代码

非常简单的形式,类似方法作为其本身的构造函数,从而定义了一个Father类,而因为js本身的propotype属性,使得可以通过原型的方式添加一些方法或者属性。关于prototype的由来以及一些小知识,详情可以阮一峰老师的博客。

www.ruanyifeng.com/blog/2011/0…

类的调用是需要先实例化的

let father = new Father('javascript', 10);
console.log(father.name, father.age) // javascript 10
复制代码
通过this定义的属性和通过prototype定义的属性有什么区别

在阮一峰老师的博客中是这样介绍的。

通过this添加的属性、方法是在当前对象上添加的,然而js是一种基于prototype的语言,所以每创建一个对象时(虽然函数在js中也是一种对象),它都有一个prototype用于指向继承的属性和方法,这样通过prototype继承的方法并不是对象自身的,所以在使用这些方法时,需要通过prototype来一级一级的查找,从而我们可以知道,this定义的属性和方法是该对象自己拥有的,所以我们每次通过类创建一个新对象的时候,this指向的属性和方法都会得到相应的创建,而通过prototype继承的属性和方法是每个对象通过prototype访问到的,即在每次通过类创建对象的时候,这些方法和属性是不会被重复创建的。

类的私有、公有如何实现

我们知道在面向对象的思想中,会有一些特殊的属性或者方法,比如c++中的public,protected以及private,然而在js中并没有这些关键字的声明。所幸的是,js本身有着其特殊的函数级作用域,即声明在函数内部的变量和方法在外界是访问不到的,所以我们可以通过这个特性来创建类的私有属性和方法;在函数内部通过this创建的属性和方法,在类创建对象的时候,每个对象自身都有一份并且外界也可以访问到,因此通过this创建的可以看作其公有的属性和方法。

let Father = function (name, age){
    let num = 1; // 私有属性
    function checkName() {} // 私有方法
    this.name = name; //公有属性
    this.getName = function() {} // 公有方法
}
复制代码
类的静态属性

通过new关键字创建新的对象的时候,由于类外面通过点语法添加的属性和方法没有执行到,所以新建的对象无法获取他们,但是可以通过类来使用,因此也被称为类的静态公有属性和静态公有方法。而通过prototype创建的属性和方法在类的实例中是可以通过this访问到的(详情请见_proto_和类的prototype之间的关系),下面这个博客讲的很清楚,www.jianshu.com/p/dee9f8b14… 因此,我们将prototype对象的属性和方法称为公有属性和公有方法。

// 静态公有属性和方法
Book.isChinese = true;
Book.resetName = function() {}
// 公有属性和方法
Book.prototype = {
    isBook: false,
    display: function() {}
}
复制代码
通过闭包实现类的静态属性和方法

闭包是指有权访问另一个函数作用域中变量的函数,即在一个函数内部创建另一个函数,我们将这个闭包作为创建对象的构造函数,这样它既是闭包又是可实例对象的函数,即可以访问到类函数作用域中的变量。

// 闭包实现
let Student = (function() {
    // 静态私有属性和方法
    var studentNum = 0;
    function checkName() {};
    // 创建类
    function _student(newName, newAge) {
        // 私有变量和方法
        var height, weight;
        function checkAge(age) {};
        // 公有属性和方法
        this.name = newName;
        this.age = newAge;
        this.copy = function() {};
    };
    _student.prototype = {
        // 静态公有属性和方法
        isStudent: false,
        display: function() {}
    };
    return _student;
})();
复制代码

以上实现使其更像一个整体。

类的检查

为了防止像我这样的新手在使用类的时候忘记实例化,通常会在类的定义中加入一些安全检查,防止报错的代码。

let Book = function(name, type) {
    // 判断执行过程中this是否是当前这个对象
    if (this instanceof Book) {
        this.name = name;
        this.type = type;
    } else {
        return new Book(name, type)
    }
}
复制代码

类的继承

通过上面的介绍我们知道,每个类有3个部分,第一部分是构造函数内的,这是供实例化对象复制使用的,第二部分是构造函数外的,直接通过点语法添加的,这是供类本身使用的,第三部分是类的prototype上的,实例化对象可以通过原型链访问到。

js本身并没有继承的概念,但是由于其特有的原型链属性,我们可以通过一些特殊的方法实现继承。

子类的原型对象——类式继承
// 声明父类
function SuperClass() {
    this.superValue = true;
}
// 父类的公有方法
SuperClass.prototype.getSuperValue = function() {
    return this.superValue
}
// 声明子类
function SubClass() {
    this.subValue = true;
}
// 继承父类
SubClass.prototype = new SuperClass();
// 子类的公有方法
SubClass.prototype.getSubValue = function() {
    return this.subValue;
}
复制代码
为什么要将父类的实例赋值给子类的原型

我们知道类的原型对象的作用就是为类的原型添加一些公有属性和方法,但是类本身是不能直接访问这些属性和方法的,必须通过原型prototype来访问。而我们在实例化一个父类的时候,新创建的对象复制了父类的构造函数内的属性和方法,并将原型_proto_指向了父类的原型对象,这样就拥有了父类原型对象上的属性和方法,并且这个新创建的对象可直接访问到父类原型上的属性和方法,如果我们将这个新创建的对象赋值给子类的prototype,那么子类的prototype就可以访问父类的prototype。即可以简单的理解为,实例化的过程,就是最简单的一次类的继承的实现。

通过instanceof判断一个类是否是另一个类的子类
let subclass = new SubClass();
console.log(subclass instanceof SuperClass); // true
console.log(subclass instanceof SubClass); // true
console.log(SubClass instanceof SuperClass); // false
复制代码

可能会有人奇怪,SubClass作为SuperClass子类的存在,为什么instanceof判断的结果会是false呢?通过之前的代码我们不难发现,实现SubClass的继承是将SuperClass的实例赋值给了SubClass的prototype,所以准确来说,是SubCLass.prototype继承了SuperClass。

console.log(SubClass.prototype instanceof SuperClass); // true
复制代码

这种实现继承的方法很简单,但是前辈们为我们总结好了它的一些缺点。其一是子类实现的继承是靠prototype对父类的实例化实现的,因此在创建父类的时候,是无法向父类传递参数的,因为在实例化父类的时候也无法对父类构造函数内的属性进行初始化。另外一点就是由于子类是通过prototype对父类实例化,所以,如果父类的公有属性中含有引用类型的属性,那么子类中对此属性的改变同样会影响到父类的公有属性(详细可见深拷贝与浅拷贝的原理)。因此,我们有了其他的继承方法。

创建即继承——构造函数继承
// 声明父类
function SuperClass(id) {
    this.id = id;
    this.book = ['js', 'css'];
}
SuperClass.prototype.showBook = function() {
    console.log(this.book)
}
// 声明子类
function SubClass(id) {
    // 继承父类
    SuperClass.call(this, id)
}
let subclass1 = new SubClass(1);
let subclass2 = new SubClass(2);

subclass1.book.push('html');
console.log(subclass1.book); // ['js', 'css', 'html']
console.log(subclass1.id); // 1
console.log(subclass2.book); // ['js', 'css']
console.log(subclass2.id); // 2

subclass1.showBook(); // TypeError
复制代码

这种继承方式的精髓就在于SuperClass.call()这个语句,call方法可以改变函数的作用环境(详情请见call和apply),因此在子类中,对SuperClass调用这个方法就是相当于将子类中的变量放在父类中执行一遍,由于父类中是给this绑定属性,因此子类自然就继承了父类的公有属性。由于这类继承没有涉及到prototype,所以父类的原型方法自然不会被子类继承,而如果想要被子类继承就必须放到构造函数中,这样创建出来的每个实例都会单独拥有而不能共用,所以综合以上两种方法各自的有点,出现了一种新的继承。

我全都要——组合继承
// 声明父类
function SuperClass(id) {
    this.id = id;
    this.book = ['js', 'css'];
}
SuperClass.prototype.showBook = function() {
    console.log(this.id);
}
// 声明子类
function SubClass(id, time) {
    // 构造函数式继承父类
    SuperClass.call(this, id);
    // 新增公有属性
    this.time = time;
}
// 类式继承 子类原型继承父类
SubClass.prototype = new SuperClass();
SubClass.prototype.getTime = function() {
    console.log(this.time);
}

let subclass1 = new SubClass(1, 2014);
subclass1.book.push('html');
console.log(subclass1.book); // ['js', 'css', 'html']
console.log(subclass1.showBook); // 1
console.log(subclass1.getTime); // 2014

let subclass2 = new SubClass(2, 2018);
console.log(subclass2.book); // ['js', 'css']
console.log(subclass2.showBook); // 2
console.log(subclass2.getTime); // 2018
subclass1.showBook(); // TypeError
复制代码

由上面的代码可以看到,子类的实例中更改父类继承下来的引用类型的属性,也没有影响到其他的实例,并且子类实例化过程中有能将参数传递到父类的构造函数中,然而,因为我们在使用构造函数继承的时候执行了一遍父类的构造函数,而在海鲜子类原型的类式继承的时候又调用了一遍父类的构造函数。因此相当于调用了两遍,一丢丢的美中不足。

最初的继承--原型式继承

在《javascript中原型继承》文章的作者写道,借助原型prototype可以根据已有的对象创建一个新的对象,同时不必创建新的自定义对象类型。

// 原型继承
function inheritObject(o) {
    // 声明一个过载函数
    function F() {};
    // 过度对象的原型继承父对象
    F.prototype = o;
    // 返回过度对象的一个实例,该实例的prototype继承了父对象
    return new F();
}
复制代码

有心的人可以发现这种继承方式和类式继承很是相似,只不过原型继承是在F过度类的构造函数中没有内容,因此开销比较小,使用起来比较方便。 而同样的,和类式继承一样,父类对象中的引用类型会公用。

升级版--寄生式继承
// 声明基对象
let book = {
    name: 'js',
    alikeBook: ['css', 'html']
};
function createBook (obj) {
    // 通过原型继承创建新对象
    let o = new inheritObject(obj);
    // 拓展新对象
    o.getName = function() {
        console.log(name);
    };
    // 返回拓展后的对象
    return o;
}
复制代码

从上面代码不难看出,寄生式继承其实就是对原型继承的二次封装,并且在二次封装的过程中对继承的对象进行了扩展,这样新创建的对象不仅有父类的属性和方法而且还会添加新的属性和方法。

最终的继承--寄生组合式继承

组合继承有个问题,那就是子类并不是父类的实例,而子类的原型式父类的实例,所以才有寄生组合式继承,那么是哪两种模式的组合呢?

寄生自然就是寄生式继承了,而由于寄生式继承依赖于原型继承,原型继承和类式继承的风格相像,那么另一种方式就不应该再是这些了,所以,另一种方式是构造函数继承,而子类不是父类实例的问题,是由于类式继承所引起的。寄生继承在此处的用法颇为奇妙,在这里他处理的不是对象,而是类的原型。

// 寄生式继承 继承原型
function inheritPrototype(subClass, superClass) {
    // 复制一份父类的原型副本保存在变量中
    let p = inheritObject(superClass.prototype);
    // 修改因为重写子类原型导致子类的constructor属性被修改
    p.constructor = subClass;
    subClass.prototype = p;
}

// 定义父类
function SuperClass(name) {
    this.name = name;
    this.colors = ['red', 'yellow'];
}
SuperClass.prototype.getName = function() {
    console.log(this.name);
}
// 定义子类
function SubClass() {
    // 构造函数继承
    SuperClass.call(this, name);
    this.time = time;
}
inheriPrototype(SubClass, SuperClass);
SubClass.prototype.getTime = function() {
    console.log(this.time);
}
复制代码

这种继承方式最大的改变就是对子类原型的处理,被赋予了父类原型的一个引用,这是一个对象,因此这里需要注意的是,子类想添加原型方法必须通过prototype对象用点语法的形式一个一个添加,否则会覆盖掉父类继承的对象。

类的多继承

js中并没有明确的多继承的概念,我在这里的理解是,浅拷贝与深拷贝的过程实现就可以看做是多继承的一种形式。

类的多态

所谓多态,就是同一个方法有不同的调用形式,js在这方面还是相当灵活的,无论是arguments对象还是es6新出的解构,都可以让我们简单灵活的通过参数来执行不同的函数,返回不同的结果。

结语

本篇文章是我的第一篇文章,因此可能会有写的不对或者不好的地方,希望大家可以指出。

有很多事情我不愿意去做,但也许那恰恰就是我最应该去做的。

转载于:https://juejin.im/post/5bdb21bae51d452bc84b552c

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值