1.面向对象编程思想

  • 面向过程:注重解决问题的步骤,复习问题需要的每一步,实现函数依次调用
  • 面向对象:是一种程序设计思想。将数据和处理数据的程序封装到对象中
  • 面向对象的特性:抽象、继承、封装、多态
  • 优点:提高代码的复用性和可维护性;

2.对象

JavaScript是一种基于对象的语言,几乎所有东西都是对象。

创建对象方法:

  1. // 1.通过字面量去创建
  2. let obj1 = { name: '张三', age: 18, type: 1 };
  3. console.log(obj1);
  4. // 2.通过 new Object() 去创建
  5. let obj2 = new Object({ name: '张三', age: 18, type: 2 });
  6. console.log(obj2);
  7. // 3.通过 Object.create() 去创建,这种创建方式和前2种有区别,区别在于创建的属性会在原型上,而不是作为自身的属性
  8. let obj3 = Object.create({ name: '张三', age: 18, type: 3 });
  9. console.log(obj3); // 打印结果为 {}, 我们定义的属性在这个空对象的原型上

3.工厂模式创建对象

工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。

工厂模式主要解决了代码冗余问题,提高了代码的复用性。

  1. // 我们想要创建很多个具有 名称、年龄、爱好的对象。如果一个个通过字面量的形式去创建会出现很多的重复代码
  2. let p1 = {
  3. name: '张三',
  4. age: 18,
  5. hobby: ()=>{
  6. console.log('我喜欢打篮球')
  7. }};
  8. let p2 = {
  9. name: '张三风',
  10. age: 19,
  11. hobby: ()=>{
  12. console.log('我喜欢打乒乓球')
  13. }};
  14. // 我们可以通过调用函数的形式,并传入对应的参数来创建对象
  15. function person(name, age, hobby){
  16. return {
  17. name,
  18. age,
  19. hobby(){
  20. console.log(hobby);
  21. }
  22. }
  23. }
  24. const zs = person('张三', 18, '我喜欢打篮球');
  25. const zsf = person('张三风', 19, '我喜欢打乒乓球');

4.构造函数

在 JavaScript 中,用 new 关键字来调用的函数,称为构造函数。

我们先来说一下 new关键字

  1. function Person(name, age, hobby){
  2. this.name = name;
  3. this.age = age;
  4. this.hobby = function (){
  5. console.log(hobby);
  6. }
  7. console.log('Person函数被执行')
  8. }
  9. // 调用 new 关键字(实例化)后,程序做了什么
  10. // 1.会执行构造函数
  11. new Person('张三', 18, '我喜欢打篮球');
  12. // 即使没有带括号,函数也会被执行
  13. new Person;
  14. // 2.自动创建一个空对象,把空对象和this绑定起来
  15. // 3.如果构造函数没有指定的返回值,隐式返回this

构造函数其实和工厂模式写法上的区别:构造函数不需要我们自己去定义空对象和返回最终结果。

  1. // 工厂模式
  2. function createPerson(name, age){
  3. // 需要自己去定义一个空对象
  4. const obj = {};
  5. obj.name = name;
  6. obj.age = age;
  7. // 需要自己手动返回创建的对象
  8. return obj;
  9. }
  10. // 调用方式和普通函数一样;
  11. const zs = createPerson('张三', 18);
  12. console.log(zs);
  13. // 构造函数
  14. function Person(name, age){
  15. // 构造函数最终由new关键字调用,它帮开发定义了一个空对象,并和this绑定,最终隐式返回this
  16. this.name = name;
  17. this.age = age;
  18. }
  19. // 调用方式,由new关键字调用函数
  20. const zsf = new Person('张三丰', 18);
  21. console.log(zsf);

实例对象

  1. // 我们通过new 构造函数的返回结果就是一个实例对象
  2. // 构造函数中的this是指向实例对象的

静态成员

  1. // 静态成员: 在构造函数本身上添加的属性或方法,只能由构造函数本身来访问,和实例化对象无关
  2. function Person(name, age){
  3. this.name = name;
  4. this.age = age;
  5. }
  6. // 静态成员,用来统计构造函数被实例化次数
  7. Person.callNum = 0;
  8. new Person('张三', 18);
  9. Person.callNum++;
  10. console.log(Person.callNum)

构造函数的性能问题

  1. function Person(name, age = 18){
  2. this.name = name;
  3. this.age = age;
  4. this.say = function (){
  5. console.log('我玩中单上分快');
  6. }
  7. }
  8. const ys = new Person('亚索');
  9. const akl = new Person('阿卡丽');
  10. // 我们发现, ys 和 akl 都有一个共同的方法say,由于引用类型的原因,它们虽然看起来一样,但是却占据了不同的内存空间
  11. console.log(ys.say === akl.say); // false
  12. // 假设我们有100个实例对象,那么就会有100个占用不同内存空间的say方法,这也就造成了性能上的浪费
  13. // 其实构造函数还给我们提供了一个公共的空间: prototype,我们可以把一些重复的属性、方法放到公共的空间来解决上面提到的问题
  14. Person.prototype.say = function (){
  15. // 这个公共空间的this也是指向实例化对象的
  16. }

5.原型

  • js分为函数对象和普通对象,每个对象都有 __proto__ 属性,但是只有函数对象才有prototype属性
  • 属性__proto__是一个对象,它有两个属性,constructor__proto__
  • 原型对象prototype有一个默认的constructor属性,用于记录实例是由哪个构造函数创建
  1. let tem;
  2. function Person(name, age = 18){
  3. this.name = name;
  4. this.age = age;
  5. }
  6. Person.prototype.fn = function (){
  7. console.log('fn');
  8. tem = this;
  9. }
  10. // 1.定义在原型对象上的方法中的this指向实例化对象
  11. const ys = new Person('亚索');
  12. ys.fn();
  13. console.log(ys === tem); // true
  14. // 2.实例化对象有 __proto__ 属性,构造函数有 prototype 属性,且两者相等
  15. console.log(ys.__proto__ === Person.prototype); // true
  16. // 3.原型对象中存在constructor属性,且constructor指向创建实例对象的构造函数
  17. // 补充:constructor 可以用来做类型判断,我们可以用这个属性来判断当前实例是那个构造函数的实例化对象
  18. console.log(Person.prototype.constructor === Person);
  19. console.log(ys.constructor === Person);

看完上面原型的介绍,我们可以再来对比一下 工厂模式和构造函数的区别

  • 工厂模式没有解决对象识别的问题,即创建的所有实例都是 Object类型 (不清楚是那个函数创建的)
  • 工厂模式没有原型,占用更多的内存空间

6.原型链

对象之间的继承关系,在 JavaScript 中是通过 prototype 对象指向父类对象,直到指向 Object 对象为止。这样就形成了一个原型指向的链条,称之为原型链。

  • 当访问一个对象属性或方法时,会现在对象自身上查找属性或方法是否存在,如果存在就使用自身的属性或方法。如果不存在就去创建对象的构造函数的原型对象中查找,依次类推,直到找到为止。如果在顶层对象中还找不到,则返回 undefined
  • 原型链最顶层为 Object 构造函数的 prototype 原型对象,给 Object.prototype 添加属性或方法可以被除 nullundefined 之外的所有数据类型对象使用。

7. JavaScript 继承

什么是继承

一个类获取另一个或者多个类的属性或者方法。继承可以使得子类具有父类的各种方法和属性。在不影响父类的前提下,减少重复的代码。

继承的原理

复制父类的方法和属性来重写子类的原型对象

法一:原型链继承

  1. function Parent () {
  2. this.name = 'kevin';
  3. this.likes = ['游戏', '旅游']
  4. }
  5. Parent.prototype.getName = function () {
  6. console.log(this.name);
  7. }
  8. function Child () {}
  9. Child.prototype = new Parent();
  10. // child可以访问父类的构造属性和原型方法,当是不能在实例化时给福构造函数传参
  11. // 还有一个缺点: 引用类型的属性被所有实例共享,如果有多个子类实例,某个实例修改了父类的引用类型的数据,那么所有实例的该属性都会发生变化
  12. const child1 = new Child();
  13. const child2 = new Child();
  14. child1.getName(); // kevin
  15. child1.name = 'test';
  16. console.log(child1.name, child2.name); // test, kevin
  17. child1.likes.push('运动');
  18. console.log(child1.likes, child2.likes); // 都打印 ['游戏', '旅游', '运动']
  19. // 优点: 写法简单
  20. // 缺点
  21. 1.父类使用this声明的属性被所有实例共享。 原因是实例化是父类一次性赋值到子类实例的原型上,它会将父类通过this声明的属性也赋值到子类原型上。例如在父类中一个数组值,在子类的多个实例中,无论哪一个实例去修改这个数组的值,都会影响到其他子类实例。
  22. 2.创建子类实例时,无法向父类构造函数传参,不够灵活。

法二:借用构造函数(call)

  1. function Parent () {
  2. this.names = ['kevin', 'daisy'];
  3. this.sayHello = function (){
  4. console.log('hello')
  5. }
  6. }
  7. function Child () {
  8. Parent.call(this);
  9. }
  10. const child1 = new Child();
  11. const child2 = new Child();
  12. child1.names.push('akl');
  13. console.log(child1.names); // ["kevin", "daisy", "akl"]
  14. console.log(child2.names); // ["kevin", "daisy"]
  15. // 优点:
  16. // 1.避免了引用类型的属性被所有实例共享
  17. // 2.可以在 Child 中向 Parent 传参
  18. // 缺点:方法都在构造函数中定义,每次创建实例都会创建一遍方法

法三:组合继承

这种方式是把法一和法二组合在一起使用

  1. function Parent (name) {
  2. this.name = name;
  3. this.colors = ['red', 'blue', 'green'];
  4. }
  5. Parent.prototype.getName = function () {
  6. console.log(this.name)
  7. }
  8. function Child (name, age) {
  9. Parent.call(this, name);
  10. this.age = age;
  11. }
  12. Child.prototype = new Parent();
  13. const child1 = new Child('kevin', '18');
  14. const child2 = new Child('daisy', '20');
  15. child1.colors.push('black');
  16. console.log(child1.name); // kevin
  17. console.log(child1.age); // 18
  18. console.log(child1.colors); // ["red", "blue", "green", "black"]
  19. console.log(child2.name); // daisy
  20. console.log(child2.age); // 20
  21. console.log(child2.colors); // ["red", "blue", "green"]
  22. // 优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。

法四:原型式继承

  1. function createObj(o) {
  2. function F(){}
  3. F.prototype = o;
  4. return new F();
  5. }
  6. // 就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。
  7. // 缺点:包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。
  8. const person = {
  9. name: 'kevin',
  10. friends: ['daisy', 'kelly']
  11. }
  12. const person1 = createObj(person);
  13. const person2 = createObj(person);
  14. person1.name = 'person1';
  15. console.log(person2.name); // kevin
  16. person1.firends.push('taylor');
  17. console.log(person2.friends); // ["daisy", "kelly", "taylor"]
  18. // 注意:修改person1.name的值,person2.name的值并未发生改变,并不是因为person1和person2有独立的 name 值,而是因为person1.name = 'person1',给person1添加了 name 值,并非修改了原型上的 name 值。

法五:寄生式继承

  1. function createObj (o) {
  2. const clone = object.create(o);
  3. clone.sayName = function () {
  4. console.log('hi');
  5. }
  6. return clone;
  7. }
  8. // 缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。

法六:寄生组合式继承

  1. // 组合继承最大的缺点是会调用两次父构造函数。
  2. // 一次是设置子类型实例的原型的时候:
  3. Child.prototype = new Parent();
  4. // 一次在创建子类型实例的时候:
  5. const child1 = new Child('kevin', '18');
  6. // 那么我们该如何精益求精,避免这一次重复调用呢?
  7. // 如果我们不使用 Child.prototype = new Parent() ,而是间接的让 Child.prototype 访问到 Parent.prototype 呢?
  8. function Parent (name) {
  9. this.name = name;
  10. this.colors = ['red', 'blue', 'green'];
  11. }
  12. Parent.prototype.getName = function () {
  13. console.log(this.name)
  14. }
  15. function Child (name, age) {
  16. Parent.call(this, name);
  17. this.age = age;
  18. }
  19. // 关键的三步
  20. const F = function () {};
  21. F.prototype = Parent.prototype;
  22. Child.prototype = new F();
  23. const child1 = new Child('kevin', '18');
  24. console.log(child1);
  1. // 封装写法
  2. function object(o) {
  3. function F() {}
  4. F.prototype = o;
  5. return new F();
  6. }
  7. function prototype(child, parent) {
  8. const prototype = object(parent.prototype);
  9. prototype.constructor = child;
  10. child.prototype = prototype;
  11. }
  12. // 当我们使用的时候:
  13. prototype(Child, Parent);
  14. // 这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。