1. 为什么要分类?

JS将数据分为七种数据类型,四基两空一对象,即四个基本数据类型number、string、bool、symbol,两个空数据类型undefined、null,一个对象数据类型。对象数据类型很复杂,但对象数据类型与现实生活的数据信息表示地最为准确,因此用到更广泛。根据实际生活中的数据有分类,JS也将对象数据类型再次细分为不同类型的对象,每类对象抽象出共同属性存放至其原型中。

JS将对象进行分类方便于管理对象,并且JS的创建之初就已经划分出了几类对象,如普通对象Object、数组队形Array、函数对象Function、日期对象Date等等。并且,JS允许用户自定义一类对象,具体实现方法可以用构造函数实现。

2. 构造函数

首先介绍一个问题引出构造函数,例如要创建5个不同宽度的正方形对象,并且每个正方形对象都有求周长和求面积的两个方法,可以想到用for循环的方式进行如下创建:

  1. let squareList = []
  2. let widthList = [1,2,3,4,5]
  3. for(let i = 0; i<5; i++){
  4. squareList[i] = {
  5. width: widthList[i],
  6. getArea(){
  7. return this.width * this.width
  8. },
  9. getLength(){
  10. return this.width * 4
  11. }
  12. }
  13. }

以上方法会浪费内存,重复创建getArea()函数和getLength()各5次,但其实这些正方形的求面积和求周长的方法是一样的,因此可以抽象出来放到正方形的原型函数中,修改后如下所示:

  1. let squareList = []
  2. let widthList = [1,2,3,4,5]
  3. let squarePrototype(){
  4. getArea(){
  5. return this.width * this.width
  6. },
  7. getLength(){
  8. return this.width * 4
  9. }
  10. }
  11. for(let i = 0; i < 5; i++){
  12. squareList[i] = Object.create(squarePrototype);
  13. squareList[i].width = widthList[i];
  14. }

以上借助原型的概念将正方形的共同属性抽象出来放到原型中,大大节省了内存空间,但以上代码还可以通过传参的形式再优化修改如下:

  1. let squareList = []
  2. let widthList = [1,2,3,4,5]
  3. let squarePrototype(){
  4. getArea(){
  5. return this.width * this.width
  6. },
  7. getLength(){
  8. return this.width * 4
  9. }
  10. }
  11. function createSquare(width){
  12. let obj = Object.create(squarePrototype);
  13. obj.width = width;
  14. return obj;
  15. }
  16. for(let i = 0; i < 5; i++){
  17. squareList[i] = createSquare(widthList[i]);
  18. }

以上代码看起来优化很多,但存在的问题是构造函数和原型函数之前看起来没有什么关系,因此可以将构造函数和其原型函数之间联系起来,采取的方法是将原型函数作为构造函数对象的一个方法,并在原型函数中记录其对应的构造函数是谁,继续修改如下:

  1. let squareList = []
  2. let widthList = [1,2,3,4,5]
  3. createSquare.squarePrototype(){
  4. getArea(){
  5. return this.width * this.width
  6. },
  7. getLength(){
  8. return this.width * 4
  9. }
  10. constructor: createSquare
  11. }
  12. function createSquare(width){
  13. let obj = Object.create(createSquare.squarePrototype); //new字符会帮忙写
  14. obj.width = width;
  15. return obj;
  16. }
  17. for(let i = 0; i < 5; i++){
  18. squareList[i] = createSquare(widthList[i]);
  19. }

因此根据以上优化后的代码,JS将其封装起来统一定义,用new操作符调用构造函数创建对象,并且将每个构造函数中的指向原型函数的属性统一定义为prototype属性,将每个prototype中指向其构造函数的属性统一定义为constructor,将构造函数和原型函数相互联系了起来,并且prototyp的constructor指向构造函数自己。因此JS在设计的过程中,给每个函数对象都添加了一个prototype属性,指向其作为构造函数时的原型对象用于存放公共属性,和proto属性没有什么关系,例如Object 的原型是指 Object.proto,不是 Object.prototype,因为 Object.prototye 是 Object 构造出来的对象的原型proto是JS规定每个对象都必须有原型并用该属性存储,函数对象也属于对象,才会有此属性。因此 构造函数的prototype === 实例对象的proto,因此写一个构造函数定义一类对象的方式如下所示,这样定义的构造函数命名首字母一般大写。

  1. let squareList = []
  2. let widthList = [1,2,3,4,5]
  3. function Square(width){
  4. this.width = width
  5. this.sum = function(){
  6. //do something
  7. }
  8. } //构造函数,其中属性为=赋值语句,添加新对象自身属性或方法
  9. Square.prototype.getArea = function(){
  10. return this.width * this.width
  11. }
  12. Square.prototype.getLength = function(){
  13. return this.width * 4
  14. } //添加新对象共用属性
  15. for(let i = 0; i < 5; i++){
  16. squareList[i] = new Square(widthList[i]);
  17. }

以上就是正确定义构造函数自定义一类对象的方法,通过new操作符省略用原型创造对象并返回对象的操作,直接让用户在构造函数中定义自己的属性,在其prototype属性中定义公共属性,并用new操作符创建对象即可。那么操作符在创建对象时做了什么?如下:

let obj = new X()

  1. 自动创建空对象
  2. 将空对象与原型关联,空对象原型地址proto值指定为X.prototype值
  3. 自动将新创建的空对象作为this关键字运行构造函数
  4. 自动return this

得到以下公式:

对象. proto=== 其构造函数.prototype

3. 代码规范

由以上构造函数的形成过程可得出一般约定俗成的命名规法如下:

  1. 构造函数的命名为名词,且首字母大写
  2. 普通函数的命名以动词开头,且首字母小写
  3. 所有被构造出来的实例对象,首字母小写。

4. class新语法

class语法是ES6新增加的语法,将上述构造函数代码修改如下:

  1. class Square{
  2. constructor(width){
  3. this.width = width
  4. } //构造函数
  5. kind = 'shape' //公有属性为赋值语句,且每个属性之间没有逗号
  6. 'kind2' = 'Rectangle'
  7. getLength:function(){
  8. return this.width * 4
  9. } //公有方法,且每个方法之间没有逗号
  10. getArea(){
  11. return this.width * this.width
  12. } //公有方法,简写定义方法,常用。
  13. get area2(){
  14. return this.width * this.width
  15. } //get 函数名,该函数设置为只读属性,调用函数area2时不用加(),只需.area2即可调用
  16. }