缘起

最近找了个兼职,写了一段node的typescript代码,虽然上次写node还是5年前,但因为使用的框架是midway,写的过程中,几乎没有碰到bug,很自然的感觉,就是spring的那一套,面向对象,依赖注入等,写web还是很方便的。

不过也由于这几年天天看和写java代码,写的时候,很容易就按照面向对象写起来,一切都是class,class的方法等,所以这也是bug的由来。

问题来龙去脉

业务bug简单描述

一个学校有n个老师,每个老师一门课程,期末的时候,老师会给学生打分,学生同时也是一个用户。

问题:在打分的过程中,为了方便,对学生对象list转换成了map,然后每个老师都会对学生的分数进行统计,然后再把统计结果返回给学生,这样就会导致学生的分数被覆盖。发现学生的成绩到最后都是一个老师打的。

复现代码

class User {
    Name: string;
    Age: number;
}

class Student extends User {
    Grade: number;
}

class Teacher {
    Name: string;
    Class: string;
    Students: Student[];
}

const user1 = new User();
user1.Name = 'John';
user1.Age = 30;


const user2 = new User();
user2.Name = 'Jane';
user2.Age = 25;



const users = [user1, user2];

const teacher = new Teacher();
teacher.Name = 'Bob';
teacher.Class = '1A';

const teacher2 = new Teacher();
teacher2.Name = 'Alice';
teacher2.Class = '1B';

const teachers = [teacher, teacher2];

const userMap = users.reduce((acc, cur) => {
    acc[cur.Name] = cur;
    return acc;
  }, {});

const gradeTeacher = new Array<Teacher>();

teachers.forEach(teacher => {
    const students = new Array<Student>();
    const teacherName = teacher.Name;

    Object.entries(userMap).forEach(([key, value]) => {

        let student = new Student();
        student = userMap[key];
        if (teacherName === 'Bob') {
          student.Grade = 100;
        } else {
            student.Grade = 50;
        }
        students.push(student);

    });
    teacher.Students = students;
    gradeTeacher.push(teacher);
});


for (const teacher of gradeTeacher) {
    console.log(teacher); 
}

理论期望的结果

Teacher {
  Name: 'Bob',
  Class: '1A',
  Students: [
    User { Name: 'John', Age: 30, Grade: 100 },
    User { Name: 'Jane', Age: 25, Grade: 100 }
  ]
}
Teacher {
  Name: 'Alice',
  Class: '1B',
  Students: [
    User { Name: 'John', Age: 30, Grade: 50 },
    User { Name: 'Jane', Age: 25, Grade: 50 }
  ]
}

实际结果

Teacher {
  Name: 'Bob',
  Class: '1A',
  Students: [
    User { Name: 'John', Age: 30, Grade: 50 },
    User { Name: 'Jane', Age: 25, Grade: 50 }
  ]
}
Teacher {
  Name: 'Alice',
  Class: '1B',
  Students: [
    User { Name: 'John', Age: 30, Grade: 50 },
    User { Name: 'Jane', Age: 25, Grade: 50 }
  ]
}

why?

可以看到,user 被循环了两次,而且每次都是user1,user2,这样就导致了学生的分数被覆盖。

let student = new Student();
student = userMap[key];
student.Grade = 100;

也就是这段代码,执行之后,student 实际变成了一个 User对象,不再是一个Student对象,同时,对student的Grade赋值的时候,ts也没有报错,类型检查的时候,认为他是Student类型,但执行的时候,认为他是User类型,同时又由于js,没有类型约束,可以赋值,所以,这里的问题就出现了。

解决方案

知道了原因就好了,我们让student获得一个深拷贝的user即可,而不是引用,这样再赋值Grade的时候,就不会出现问题。

let student = new Student();
Object.assign(student, userMap[key]);
student.Grade = 100;

后记

JavaScript写业务,非强类型语言,还是要注意类型的约束,不然会出现一些问题。所以,在写业务的时候,Java和Go还是更加安全一些。

同时typeScript也是一个静态类型语言,可以更加的更加安全一些。但IDE的提示,还有程序员自己的熟练程度也是影响代码质量的一个很重要的因素,如果typeScript编译没有检查出来,则实际执行的过程中,也会出现问题。