进来聊聊 class 类,基础更上一层楼

356 阅读9分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第2天,点击查看活动详情

上一篇我们讲了面向对象的具体含义,其中包括了class的相关概念,那么这一篇文章我们来继续深入的理解一下class中的其他奥妙。

首先 class 并不是标签上的属性,而是 javascript中关于模块定义的一个语法糖,我们先来看看官方定义

类是用于创建对象的模板。他们用代码封装数据以处理该数据。 JS中的类建立在原型上,但也具有某些语法和语义未与ES5类相似语义共享。

它就是一个特殊的函数,只不过定义的方式不同,关于类的定义和关键字的说明请我的上一篇文章 面向对象编程,你真正懂吗

好的费话不多少,开始讲解

ES6中的类和对象

构造函数和原型

在ES6之前,对象不是基于类创建的,而是用一种称为构造函数的特殊函数来定义对象和它们的特征。

构造函数是一种特殊的函数,主要用来初始化对象,即为对象成员变量赋初始值,它总与new一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到函数里面。

在JS 中,使用构造函数时要注意以下两点:

  1. 构造函数用于创建某一类对象,其首字母要大写

  2. 构造函数要和new 一起使用才有意义

这里有个很重要的考点new在执行时会做四件事情

  1. 在内存中创建一个新的空对象。
  2. this指向这个新的对象。
  3. 执行构造函数里面的代码,给这个新对象添加属性和方法。
  4. 返回这个新对象(所以构造函数里面不需要return)。

静态成员和实例成员

JavaScript 的构造函数中可以添加一些成员,可以在构造函数本身上添加,也可以在构造函数内部的this 上添加。通过这两种方式添加的成员,就分别称为静态成员和实例成员。

  • 静态成员:在构造函数本身上添加的成员称为静态成员,只能由构造函数本身来访问
  • 实例成员:在构造函数内部创建的对象成员称为实例成员,只能由实例化的对象来访问

举个🌰

function Person (uname, age) {
    this.uname = uname;
    this.age = age;
    this.say = function () {
        console.log(123);
    }
}

var obj = new Person('张三丰',22);
console.log(obj.uname); // 张三丰
console.log( Person.uname ); // undefined

Person.leibie = '人';
console.log(Person.leibie); // 人
console.log(obj.leibie); // undefined

这里有个小疑问

当实例化对象的时候,属性好理解,属性名属性值,那么方法是函数,函数是复杂数据类型。那么保存的时候是保存地址,又指向函数,而每创建一个对象,都会有一个函数,每个函数都得开辟一个内存空间,此时浪费内存了,那么如何节省内存呢?

这时我们就需要用到原型

构造函数原型 prototype

什么是原型对象:就是一个属性,是构造函数的属性,这个属性是一个对象,我们也称呼,prototype 为原型对象。

作用:是为了共享方法,从而达到节省内存

注意:每一个构造函数都有prototype属性

构造函数通过原型分配的函数是所有对象所共享的。

JavaScript 规定,每一个构造函数都有一个prototype 属性,指向另一个对象。注意这个prototype 就是一个对象,这个对象的所有属性和方法,都会被构造函数所拥有。我们可以把那些不变的方法,直接定义在prototype 对象上,这样所有对象的实例就可以共享这些方法。

function Star (uname, age) {
    this.uname = uname;
    this.age = age;
    // this.sing = function () {
        // console.log(this.name + '在唱歌');
    // }
}

Star.prototype.sing = function () {
console.log(this.uname + '在唱歌');
}

var zxc = new Star('周星驰', 22);
var ldh = new Star('刘德华', 22);
// console.log( Star.prototype );
ldh.sing();
zxc.sing();

内存.jpg

总结:所有的公共属性写到构造函数里面,所有的公共方法写到原型对象里面

又有一个疑问:为何创建一个对象,都可以自动的跑到原型对象上找方法

因为每一个对象都有一个属性,proto 对象原型,执行原型对象

对象原型:__proto __

主要作用:指向prototype

构造函数和原型对象都会有一个属性 proto 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在。

注意:

  1. ____proto____是一个非标准属性,不可以拿来赋值或者设置【只读属性】
  2. ____proto____对象原型和原型对象prototype 是等价的
  3. ____proto____对象原型的意义就在于为对象的查找机制提供一个方向,或者说一条路线,但是它是一个非标准属性,因此实际开发中,不可以使用这个属性,它只是内部指向原型对象prototype

关系图.jpg

总结:每一个对象都有一个原型,作用是指向原型对象prototype

统一称呼:proto原型,prototype成为原型对象

构造函数 constructor

记录是哪个构造函数创建出来的

指回构造函数本身

原型 proto 和构造函数 prototype 原型对象里面都有一个属性constructorconstructor 我们称为构造函数,因为它指回构造函数本身。constructor 主要用于记录该对象引用于哪个构造函数,它可以让原型对象重新指向原来的构造函数。一般情况下,对象的方法都在构造函数的原型对象中设置。如果有多个对象的方法,我们可以给原型对象采取对象形式赋值,但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象constructor 就不再指向当前构造函数了。此时,我们可以在修改后的原型对象中,添加一个constructor 指向原来的构造函数。

总结:constructor 主要作用可以指回原来的构造函数

看到这是不是已经乱了, 那么接下来就说一下它们之间的关系到底是怎样的相辅相成的

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

构造函数,原型对象,对象实例关系.jpg

原型链

作用:提供一个成员的查找机制,或者查找规则

原型链:查找属性或者方法的时候,由_____proto____组成的一条链

原型链.jpg

总结

  • 构造函数:

    • 方法和属性:
      • 构造函数里面放属性,
      • 原型对象里面放方法
  • 原型对象:是构造函数的一个属性,【每一个构造函数都有原型对象】,作用:放方法【prototype】

  • 对象原型:是对象的一个属性,【每一个对象都有对象原型】,作用:指向原型对象【__proto __

  • 构造函数:constructor:是原型对象的一个属性,作用:指回构造函数

  • 原型链:原型组成的一个查找成员机制的链(路线),作用:查找成员的机制

JavaScript 的成员查找机制(规则)

当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。

如果没有就查找它的原型(也就是__proto__指向的prototype 原型对象)。

如果还没有就查找原型对象的原型(Object的原型对象)。

依此类推一直找到Object 为止(null)。

__proto__对象原型的意义就在于为对象成员查找机制提供一个方向,或者说一条路线。

console.log(Star.prototype.__proto__.__proto__);
console.log(Object.prototype);

扩展内置对象

可以通过原型对象,对原来的内置对象进行扩展自定义的方法。比如给数组增加自定义求偶数和的功能。

扩展:最大值最小值

console.log( Array.prototype );
    // 添加求和方法
    Array.prototype.sum = function () {
            var sum = 0;
            for (var i = 0; i < this.length; i++) {
                    sum += this[i];
            }
            return sum;
    }

    var arr = [1,2,3];
    console.log( arr.sum() );

    var newArr = [6,7,8,9];
    console.log( newArr.sum() );

继承

关于继承的方法有很多,网上的整理也比较全面,本小节只谈论class中的继承

ES6之前并没有给我们提供extends 继承。我们可以通过构造函数+原型对象模拟实现继承,被称为组合继承。

call()

调用这个函数, 并且修改函数运行时的this 指向

fun.call(thisArg, arg1, arg2, ...);call把父类的this指向子类

thisArg :当前调用函数this 的指向对象

arg1,arg2:传递的其他参数

属性的继承

利用构造函数实现属性的继承

function Father (uname,age) {
    // this指向父类的实例对象
    this.uname = uname;
    this.age = age;
    // 只要把父类的this指向子类的this既可
}
function Son (uname, age,score) {
    // this指向子类构造函数
    // this.uname = uname;
    // this.age = age;
    // Father(uname,age);
    Father.call(this,uname,age);
    this.score = score;
}
Son.prototype.sing = function () {
    console.log(this.uname + '唱歌')
}
var obj = new Son('刘德华',22,99);
console.log(obj.uname);
console.log(obj.score);
obj.sing();

方法的继承

实现方法把父类的实例对象保存给子类的原型对象

一般情况下,对象的方法都在构造函数的原型对象中设置,通过构造函数无法继承父类方法。核心原理:

①将子类所共享的方法提取出来,让子类的prototype 原型对象= new 父类()

②本质:子类原型对象等于是实例化父类,因为父类实例化之后另外开辟空间,就不会影响原来父类原型对象

③将子类的constructor 指回本身

function Father () {

}

Father.prototype.chang = function () {
    console.log('唱歌');
}

function Son () {

}

// Son.prototype = Father.prototype;
Son.prototype = new Father();
var obj = new Son();
obj.chang();

Son.prototype.score = function () {
        console.log('考试');
}

// obj.score();
// console.log(Son.prototype);
console.log(Father.prototype);

注意:一定要让Son指回构造函数

实现继承后,让Son指回原构造函数

Son.prototype = new Father();

Son.prototype.constructor = Son;

总结:用构造函数实线属性继承,用原型对象实线方法继承

类的本质

class本质还是function

类的所有方法都定义在类的prototype属性上

类创建的实例,里面也有__proto__指向类的prototype原型对象

所以ES6的类它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。

所以ES6的类其实就是语法糖.

语法糖:语法糖就是一种便捷写法. 简单理解, 有两种方法可以实现同样的功能, 但是一种写法更加清晰、方便,那么这个方法就是语法糖