进击JavaScript核心 --- (3)面向对象

JS中的对象定义为:无序属性的结合,其属性可以包含基本值、对象或者函数
 
1、定义对象的方式
 
(1)、Object构造函数
var student = new Object();
student.name = 'Jim Green';
student.gender = 'Male';
student.age = 8;
student.say = function() {
  console.log(`My name is ${this.name}, I'm ${this.age} years old`);
}

student.say();      // My name is Jim Green, I'm 8 years old

 

(2)、对象字面量

var student = {
  name: 'Jim Green',
  gender: 'Male',
  age: 8,
  say: function() {
    console.log(`My name is ${this.name}`)
  }
}

student.say();    // My name is Jim Green

 

2、属性类型

Object.defineProperty()方法:用于修改对象属性的默认特性
这个方法接收三个参数:属性所在的对象,属性的名字和一个描述符对象。其中,描述符对象必须是以下两种之一(不能同时是两者)
 
2-1、数据属性
数据属性包含一个数据值的位置,在这个位置可以读取和写入值
 
configurable:默认值为true。表示能否通过delete删除该属性,能否修改属性的特性,或者能否把属性修改为访问器属性
enumerable:默认值为true。表示能否通过 for-in 循环返回属性
writable:默认值为true。表示能否修改属性值
value:默认值为undefined。包含这个属性的属性值,读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置
var person = {
  age: 8,
  gender: 'male'
};
Object.defineProperty(person, "name", {
  enumerable: false,
  value: "Roger"
});
for(var key in person) {
  console.log(`key --> ${key}, value --> ${person[key]}`);
}

// key --> age, value --> 8
// key --> gender, value --> male

 

如果将name属性的configurable值改为false,并尝试删除该属性,非严格模式下无效,delete操作被忽略;严格模式下报错
// "use strict";
var person = {};
Object.defineProperty(person, "name", {
  configurable: false,
  writable: true,
  value: "Roger"
});
console.log(person.name);       // Roger
delete person.name;
console.log(person.name);       // Roger    (configurable属性为false时,返回undefined)

// Uncaught TypeError: Cannot delete property 'name' of # (严格模式下报错) 
          
         

 

 
通过修改name属性的writable将其变为只读属性,非严格模式下修改该属性无效,赋值操作被忽略;严格模式下会报错
// "use strict"
var person = {};
Object.defineProperty(person, "name", {
  writable: false,
  value: "Roger"
})

console.log(person.name);       // Roger
person.name = "Frank";
console.log(person.name);       // Roger 

// console.log(person.name);       // Uncaught TypeError: Cannot assign to read only property 'name' of object '#' 
          
         

 

注意:
(1)、调用Object.defineProperty()方法时,如果不指定,configurable、enumerable和writable特性的默认值就都是false
var person = {};
Object.defineProperty(person, "name", {
  configurable: true,
  value: "Roger"
});
person.name = "Frank";
console.log(person.name);                // Roger

Object.defineProperty(person, "name", {
  writable: true,
  value: "Roger"
});

person.name = "Kobe";                   
console.log(person.name);                // Kobe

 

(2)、一旦把属性定义为不可配置的(configurable为false),就不能再把它设为可配置了。此时,再修改除 writable之外的特性,都会导致报错
var person = {};
Object.defineProperty(person, "name", {
  configurable: false,
  value: "Roger"
});
person.name = "Frank";
console.log(person.name);                // Roger

Object.defineProperty(person, "name", {  // Uncaught TypeError: Cannot redefine property: name
  writable: true,
  value: "Roger"
});

person.name = "Kobe";                   
console.log(person.name);

 

2-2、访问器属性

访问器属性不包含数据值,它们包含一对getter和setter函数(非必需)。共包含4个特性:

 
configurable:默认值为true;表示能否通过delete删除属性,能否修改属性的特性,或者能否把属性修改为数据属性
enumerable:默认值为true;表示能否通过for-in循环此属性
get:默认值为undefined;在读取属性时调用函数
set:默认值为undefined;在写入属性时调用函数
var person = {
  name: 'Kobe',
  _number: 8
}

Object.defineProperty(person, 'number', {
  get: function() {
    return this._number;
  },
  set: function(newValue) {
    if(newValue > 8) {
      this._number = 24;
    }
  }
})

person.number = 9;
console.log(person._number);          // 24

 

Object.defineProperties() 方法:可以通过描述符一次定义多个属性,这个方法接收两个对象参数
 
第一个对象是要添加和修改其属性的对象
第二个对象与第一个对象中要添加或修改的属性一一对应
var book = {};
Object.defineProperties(book, {
  _year: {
    writable: true,
    value: 8
  },
  year: {
    get: function() {
      return this._year;
    },
    set: function(newValue) {
      if(newValue > 8) {
        alert('111')
        this._year = 24;
      }
    }
  }
})

console.log(book);    // {_year: 8}
book.year = 9;
console.log(book);    // {_year: 24}

 

Object.getOwnPropertyDescriptor() 方法用于读取给定属性的描述符,这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称,返回值是一个对象。
 
如果是访问器属性,这个对象包含 configurable, enumerable, get, set;
如果是数据属性,这个对象包含 configurable,enumerable,writeable,value;
var book = {};
Object.defineProperties(book, {
  _year: {
    writable: true,
    value: 8
  },
  year: {
    get: function() {
      return this._year;
    },
    set: function(newValue) {
      if(newValue > 8) {
        alert('111')
        this._year = 24;
      }
    }
  }
});

var obj1 = Object.getOwnPropertyDescriptor(book, '_year');
var obj2 = Object.getOwnPropertyDescriptor(book, 'year');

console.log(obj1);   // {value: 8, writable: true, enumerable: false, configurable: false}
console.log(obj2);   // {get: ƒ, set: ƒ, enumerable: false, configurable: false}

 

3、创建对象
 
面向对象的编程语言都有类的概念,通过类可以创建任意多个具有相同属性和方法的对象
// 例如:Java中定义一个Student类
public class Student {

  private String name; // 姓名
  private int age; // 年龄

  public Student(String name, int age) { //构造器
    super();
    this.name = name;
    this.age = age;
  }
  
  // 属性的getter和setter方法
  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public int getAge() {
    return age;
  }

  public void setAge(int age) {
    this.age = age;
  }
  
  // 自定义方法
  public void say() {
    System.out.println("姓名:" + this.name + "年龄:" + this.age);
  }

}

// 通过Student类来创建实例对象
Student xiaoMing = new Student("XiaoMing", 12);
Student xiaoHong = new Student("XiaoHong", 9);

xiaoMing.say();   // 姓名:XiaoMing年龄:12
xiaoHong.say();   // 姓名:XiaoHong年龄:9

 

如果是通过JS来创建两个学生对象,可以用对象字面量或者Object构造函数创建单个的对象:
var xiaoMing = {
  name: "XiaoMing",
  age: 12
};


var XiaoHong = {
  name: "XiaoHong",
  age: 9
}

 

这么做有两个明显的弊端,一是重复的代码太多,如果一个班级有60名学生,就要重复60次;二是可读性差,只知道是个对象,到底是个什么对象,学生的集合,人的集合还是其它?
 
3-1、工厂模式
 
用函数来封装以特定接口创建对象的细节
function createStudent(name, age) {
  var obj = new Object();
  obj.name = name;
  obj.age = age;
  obj.say = function() {
    console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
  };
  return obj;
}

var xiaoMing = createStudent('XiaoMing', 12);
var xiaoHong = createStudent('XiaoHong', 8);

xiaoMing.say();     // My name is XiaoMing, I'm 12 years old.
xiaoHong.say();     // My name is XiaoHong, I'm 8 years old.
弊端:这样做虽然很好的解决了代码重复的问题,但还是不知道创建的到底是个什么类型的对象
 
3-2、构造函数模式
 
使用自定义构造函数来定义对象类型的属性和方法,通常为了区别于普通函数,会将构造函数的首字母大写
function Student(name, age) {
  this.name = name;
  this.age = age;
  this.say = function() {
    console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
  };
}

var xiaoMing = new Student('XiaoMing', 12);
var xiaoHong = new Student('XiaoHong', 8);

xiaoMing.say();     // My name is XiaoMing, I'm 12 years old.
xiaoHong.say();     // My name is XiaoHong, I'm 8 years old.

 

与工厂模式相比,有以下几个不同的地方:
-、没有显示的创建对象
-、直接将属性和方法赋给了this对象
-、没有return语句

使用new操作符创建自定义对象,主要经历了4个步骤:
-、创建一个新对象
-、将this指向新对象
-、为新对象添加属性
-、返回新对象

对象都有一个constructor属性用于标识对象的类型,该属性指向创建对象的构造函数
console.log(xiaoMing.constructor)

// ƒ Student(name, age) {
//   this.name = name;
//   this.age = age;
//   this.say = function() {
//     console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
//   };
// }

console.log(xiaoMing.constructor == Student)                  // true
console.log(xiaoMing.constructor == xiaoHong.constructor)     // true
console.log(xiaoMing instanceof Student)                      // true
console.log(xiaoMing instanceof Object)                       // true

 

构造函数与普通函数的唯一区别,就在于调用方式。任何函数,只要使用new操作符来调用就可以作为构造函数,如果不通过new操作符来调用,那就是普通函数
function Student(name, age) {
  this.name = name;
  this.age = age;
  this.say = function() {
    console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
  };
}


// 将Student当作构造函数(this指向实例对象)
var xiaoMing = new Student('XiaoMing', 12);   
xiaoMing.say();     // My name is XiaoMing, I'm 12 years old.


// 将Student当作普通函数(由于Student函数属于全局作用域,因此实际上是window.Student(),this指向window)
Student('Bob', 9);
say();              // My name is Bob, I'm 9 years old.


// 在特定的作用域中调用函数(this指向obj)
var obj = new Object();
Student.call(obj, 'Ryan', 30);
console.log(obj);       // {name: "Ryan", age: 30, say: ƒ}
obj.say();              // My name is Ryan, I'm 30 years old.
 
弊端:由于方法也是对象(如果对象的属性是一个函数就称之为方法),这就意味着每次实例化一个对象,都重新创建了一个对象
// 上面的say方法等价于
function Student(name, age) {
  this.name = name;
  this.age = age;
  this.say = new Function(`My name is ${this.name}, I'm ${this.age} years old.`);
}

 

为了解决这个问题,似乎可以把方法提出来
function Student(name, age) {
  this.name = name;
  this.age = age;
  this.say = say;
}
function say() {
  console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
}

var xiaoHong = new Student('XiaoHong', 8);
xiaoHong.say();       // My name is XiaoHong, I'm 8 years old.

这样做的话同样存在问题,首先是封装性不太好,对象的某些属性必须依赖于全局的属性;其次,我们期望全局作用域内的函数say只能用于构造函数Student,这样就跟js的理念相冲突了

 

3-3、原型模式

每个函数都有一个prototype属性,prototype属性是一个指针,指向函数的原型对象,该对象包含所有实例对象共享的属性和方法
function Student() {

}
Student.prototype.name = 'Bob';
Student.prototype.age = 12;
Student.prototype.say = function() {
  console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
}

var xiaoMing = new Student();
var xiaoHong = new Student();

xiaoMing.say();   // My name is Bob, I'm 12 years old.
xiaoHong.say();   // My name is Bob, I'm 12 years old.

 

与构造模式不同的时,所有实例对象访问的都是相同的属性和同一个say方法

对比prototype和constructor
prototype:每个函数都有一个 prototype属性,指向一个原型对象
constructor:每个对象都有一个 constructor属性,指向创建该对象的构造函数
进击JavaScript核心 --- (3)面向对象_第1张图片
通过构造函数创建的实例对象内部都包含一个指针(内部属性),指向构造函数的原型对象,这个指针就是 [[Prototype]]。换句话说,实例对象与构造函数没有直接关系
但是,脚本中没有标准的方式访问[[Prototype]],一些现代浏览器中可以通过__proto__属性来访问
console.log(xiaoMing.__proto__);      // {name: "Bob", age: 12, say: ƒ, constructor: ƒ}
console.log(xiaoMing.__proto__ === Student.prototype);  // true

__proto__属性是一个访问器属性,其中包含了getter(读取器)和 setter(设置器)
getter:暴露了一个对象的内部[[Prototype]],这个值就是构造器函数的prototype属性,如:Array.prototype, Object.prototype
setter:允许对象的[[Prototype]]被更改
进击JavaScript核心 --- (3)面向对象_第2张图片
虽然无法直接访问 [[Prototype]] 属性,但可以通过 isPrototypeOf() 方法来测试一个对象是否存在于另一个对象的原型链上

prototypeObj.isPrototypeOf(object)
参数:在object对象的原型链上搜寻
返回值:Boolean
console.log(Student.prototype.isPrototypeOf(xiaoMing));   // true
console.log(Student.prototype.isPrototypeOf(xiaoHong));   // true
// 说明实例对象 xiaoMing和xiaoHong都存在于Student.prototype的原型链上

 

尽管部分现代浏览器都实现了__proto__属性,但是该属性从未被包含在ECMA规范中,因此不推荐使用。

从ES5开始,[[Prototype]] 可以通过 Object.getPrototypeOf() 访问器访问,用于返回指定对象的原型
console.log(Object.getPrototypeOf(xiaoHong));  // {name: "Bob", age: 12, say: ƒ, constructor: ƒ}
console.log(Object.getPrototypeOf(xiaoHong) === Student.prototype);   // true

 

当为某个实例对象添加同名属性时,这个属性就会屏蔽掉原型对象中保存的同名属性,也就是说,如果实例对象自己有某个属性,就不会去它的原型对象上找
xiaoMing.name = 'XiaoMing';

console.log(xiaoMing.name);   // XiaoMing
console.log(xiaoHong.name);   // Bob

 

为了进一步对比,可以删除掉实例对象 xiaoMing 的属性name,然后再访问该属性

delete xiaoMing.name;

console.log(xiaoMing.name);   // Bob
console.log(xiaoHong.name);   // Bob

 

当前测试的是属性值是基本类型的情况, 如果属性值是引用类型就不再屏蔽了
function Student() {
  
}
Student.prototype = {
  name: 'Bob',
  age: 12,
  course: ['Chinese', 'Math']
}

var xiaoMing = new Student();
var xiaoHong = new Student();

xiaoMing.name = 'XiaoMing';
console.log(xiaoMing.name);       // XiaoMing
console.log(xiaoHong.name);       // Bob

xiaoMing.course.push('English');
console.log(xiaoMing.course);     // ["Chinese", "Math", "English"]
console.log(xiaoHong.course);     // ["Chinese", "Math", "English"]

 

如果需要判断一个属性是存在于实例对象中,还是原型对象中,可以使用 obj.hasOwnProperty(prop) 方法
xiaoMing.name = 'XiaoMing';

console.log(xiaoMing.hasOwnProperty('name'));   // true     --- 来自实例
console.log(xiaoHong.hasOwnProperty('name'));   // false    --- 来自原型
// 实例对象xiaoMing有自己的name属性,xiaoHong则没有 

 

in操作符:只要通过对象能够访问到给定的属性(不管是对象自身的属性还是原型对象的属性),都返回true
hasProperty():只有属性存在于对象时才返回true
xiaoMing.name = 'XiaoMing';

console.log('name' in xiaoMing);                // true
console.log(xiaoMing.hasOwnProperty('name'));   // true

console.log('name' in xiaoHong);                // true
console.log(xiaoHong.hasOwnProperty('name'));   // false

 

因此,要判断一个属性是原型中的属性,只需要同时满足in返回true,hasOwnProperty()返回false即可
xiaoMing.name = 'XiaoMing';

// 判断一个属性仅存在于对象的原型中
function checkPropertyInPrototype(Object, prop) {
  return (prop in Object) && !Object.hasOwnProperty(prop)
}

console.log(checkPropertyInPrototype(xiaoMing, 'name'));    // false
console.log(checkPropertyInPrototype(xiaoHong, 'name'));    // true

 

for-in 循环不仅会遍历对象自身的属性,还会遍历对象原型的属性(这里说的属性必须是可枚举的)
function Student() {

}
Student.prototype.name = 'Bob';
Student.prototype.age = 12;
Student.prototype.say = function() {
  console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
}

var xiaoHong = new Student();
xiaoHong.gender = 'female';

for(var prop in xiaoHong) {
  console.log(prop +' --> '+ xiaoHong[prop]);
}

/*
gender --> female
name --> Bob
age --> 12
say --> function() {
  console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
}
*/

 

弊端:所有实例只能共享属性,无法通过构造函数初始化参数
 
3-4、组合构造函数模式和原型模式
 
这也是创建自定义类型的最常见方式,构造函数模式用于定义实例属性,原型模式用于定义方法和共有的属性
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype = {
  say: function() {
    console.log(`My name is ${this.name}, I'm ${this.age} years old.`);
  }
}

var xiaoMing = new Person('XiaoMing', 12);
var xiaoHong = new Person('xiaoHong', 9);

xiaoMing.say();     // My name is XiaoMing, I'm 12 years old.
xiaoHong.say();     // My name is xiaoHong, I'm 9 years old.

 

4、继承

JS中的继承主要是依靠原型链来实现的

4-1、原型链

简单回顾下构造函数、原型和实例对象的关系

(1)、每个构造函数都包含一个prototype属性指向原型对象

(2)、每个原型对象都包含一个constructor指针指向构造函数

(3)、每个实例都包含一个[[prototype]]指针指向构造函数的原型对象

原型链的基本思想就是利用原型,让一个引用类型继承另一个引用类型的属性和方法

具体说就是让原型对象等于另一个类型的实例,由于该实例包含指向其原型对象的指针,因此,此时的原型对象也包含了另一个原型对象的 指针。层层向上直到一个对象的原型对象是null。根据定义,null没有原型,并作为原型链中的最后一个环节

function Person() {
    
  }
  Person.prototype = {
    leg: 4,
    ear: 2
  }

  function Student() {
    
  }

  // 让Student的原型对象等于Person的实例,因此Student的原型对象也包含了指向Person的原型对象的指针
  Student.prototype = new Person();

  // 给Student的原型对象添加方法
  Student.prototype.say = function() {
    console.log(`I have ${this.ear} ears and ${this.leg} legs.`);
  }

  var student = new Student();
  student.say();   // I have 2 ears and 4 legs. 
  console.log(student.toString());  // [object Object]

  console.log(Object.getPrototypeOf(student) === Student.prototype);  // true (student实例的原型对象就是Student.prototype)
  console.log(Person.prototype.isPrototypeOf(student));               // true (student实例存在于Person对象的原型链上)
  console.log(student instanceof Person);                             // true (student就是Person对象的实例)
  console.log(student.hello());                                       // Uncaught TypeError: student.hello is not a function

进击JavaScript核心 --- (3)面向对象_第3张图片

图中所示,红色的粗线条代表的就是原型链,绿色细线条代表是构造函数与其原型对象之间的关联。

执行 student.say(),student实例自己没有say方法,于是沿着原型链在它的原型对象是找到了,但是该原型对象中并没有ear和leg属性,由于该原型对象包含了指向Person原型对象的指针,因此,继续沿着原型链向上查找,在Person的原型对象上找到了ear和leg属性

执行 student.toString(),student实例没有提供该方法,于是沿着原型链逐级向上查找,由于所有引用类型都继承自Object,最终在Object的原型对象上找到了

执行 student.hello(),student实例没有提供该方法,沿着原型链逐级向上查找,都没有找到该方法,因此会报错

 

注意:

(1)、通过原型链实现继承时,不能使用对象字面量创建原型方法,否则会重写原型链

function Person() {
    
}
Person.prototype = {
  say: function() {
    console.log('hello world');
  }
}

function Student() {
  
}

// 让Student的原型对象等于Person的实例,因此Student的原型对象也包含了指向Person的原型对象的指针
Student.prototype = new Person();

// 使用对象字面量给Student创建原型方法
Student.prototype = {
  class: 2,
  grade: 1
}

var student = new Student();
student.say();                // Uncaught TypeError: student.say is not a function

进击JavaScript核心 --- (3)面向对象_第4张图片

此例中,先是把Person的实例赋值给Student的原型对象,构建出了一条图中红色粗线部分显示的原型链。然后,使用对象字面量的方法使得Student的原型对象指向了一个实例对象,切断了原来的原型链,重新构建出图中蓝色粗线部分的原型链,自然就找不到say方法了

(2)、给原型添加方法的代码一定要放在替换原型的语句之后。如子类型重写父类型中的某个方法,或在子类型中添加一个父类型中不存在的方法

function Person() {
    
}
Person.prototype = {
  say: function() {
    console.log('hello world');
  }
}

function Student() {
  
}

// 让Student继承Person
Student.prototype = new Person();

// 子类型重写父类型中的方法
Student.prototype.say = function() {
  console.log(`I'm Iron man`)
}

// 子类型中添加一个父类型中不存在的方法
Student.prototype.walk = function() {
  console.log('walk with legs')
}

var student = new Student();

student.say();                // I'm Iron man
student.walk();               // walk with legs

进击JavaScript核心 --- (3)面向对象_第5张图片

首先确定了Student继承Person这一继承关系,Student原型对象可以读取到Person原型对象中的say方法,然后为Student原型对象添加的重写和新方法,会覆盖掉原来的say方法

function Person() {
    
}
Person.prototype = {
  say: function() {
    console.log('hello world');
  }
}

function Student() {
  
}

// 子类型重写父类型中的方法
Student.prototype.say = function() {
  console.log(`I'm Iron man`)
}

// 子类型中添加一个父类型中不存在的方法
Student.prototype.walk = function() {
  console.log('walk with legs')
}

// 让Student继承Person
Student.prototype = new Person();

var student = new Student();


console.log(Person.prototype.isPrototypeOf(student));  // true
student.say();                // hello world
student.walk();               // Uncaught TypeError: student.walk is not a function

与前面唯一的区别就是继承关系是在Student原型对象添加方法之后确定的,尽管student实例和Person的原型对象依然在同一条原型链上,但是会用Person原型对象中的属性和方法覆盖掉Student原型对象中的属性和方法,导致输出和前面的不一样

原型链弊端:
(1)、针对属性值是引用类型的情况,当某一个实例对象改变该共享属性时,其它实例也会随之改变

function Person() {
  this.course = ['chinese', 'math'];
}
function Student() {

}
Student.prototype = new Person();

var student1 = new Student();

console.log(student1.course);      // ["chinese", "math"]

student1.course.push('english');
var student2 = new Student();
console.log(student2.course);      // ["chinese", "math", "english"]

 

(2)、创建子类型的实例时,不能向父类型的构造函数中传递参数。实际上就是一旦给父类型的构造函数传递参数,就会影响所有的实例对象

function Person(name, age) {
  this.name = name;
  this.age = age;
}
function Student() {

}
// 此处调用父类型的构造函数是需要传参的
Student.prototype = new Person('Bob', 12); 

var s1 = new Student();
var s2 = new Student();

console.log(s1.name);     // Bob
console.log(s2.name);     // Bob

 

4-2、借用构造函数

用于解决原型中包含引用类型值所带来的问题

基本思想是在子类型构造函数中调用父类型的构造 函数,通过call()或apply()方法在(将来)新创建的对象上执行构造函数

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.say = function() {
    console.log(`My name is ${this.name}, I'm ${this.age} years old`)
  }
}

function Student(name, age, grade) {
  Person.call(this, name, age);
  this.grade = grade;
}
function Teacher(name, age, height) {
  Person.apply(this, [name, age]);
  this.height = height;
}

var student = new Student("Jim", 9, 4);
var teacher = new Teacher('Mr Lee', 33, 1.75);

student.say();  // My name is Jim, I'm 9 years old
teacher.say();  // My name is Mr Lee, I'm 33 years old

通过这种方式,子类型不仅可以继承父类型,还可以向父类型构造函数传递参数

如何解决原型中包含引用类型值带来的问题?

function Person() {
  this.course = ['chinese', 'math'];
}
function Student() {
  Person.call(this);
}

var s1 = new Student();

console.log(s1.course);       // ["chinese", "math"]
s1.course.push('english');
console.log(s1.course);       // ["chinese", "math", "english"]

var s2 = new Student();
console.log(s2.course);       // ["chinese", "math"]

利用call方法将this绑定到了实例对象,所以即便修改了引用类型的值,也只是在实例对象的作用域范围内,不影响其他实例

弊端:方法都在构造函数中定义,如果在父类型的原型对象中定义方法,对子类型还是不可见的

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.say = function() {
  console.log(`My name is ${this.name}`);
}

function Student(name, age) {
  Person.call(this, name, age);
}

var s = new Student("Jim", 2);
s.say();     // Uncaught TypeError: s.say is not a function

 

4-3、组合继承

就是将原型链和借用构造函数两种方法组合在一起实现继承,这也是JS中最常用的继承模式

思路:使用原型链实现对原型属性和方法的继承,通过借用构造函数实现对实例属性的继承

function Person(name) {
  this.name = name;
  this.course = ['chinese', 'math'];
}
Person.prototype.say = function() {
  console.log(`My name is ${this.name}`);
}

function Student(name, age) {
  Person.call(this, name);     // 继承实例属性
  this.age = age;
}

// 继承原型属性和方法
Student.prototype = new Person();

// 添加子类方法
Student.prototype.sayAge = function() {
  console.log(`I'm ${this.age} years old`);
}

// 重写父类方法
Student.prototype.say = function() {
  console.log(`My name is ${this.name}, I'm ${this.age} years old`);
}

var s1 = new Student('Bob', 8);

// 修改引用类型值的属性
s1.course.push('english');

s1.sayAge();                  // I'm 8 years old
s1.say();                     // My name is Bob, I'm 8 years old
console.log(s1.course);       // ["chinese", "math", "english"]

var s2 = new Student('Jim', 11);
console.log(s2.course);       // ["chinese", "math"]

 

 

转载于:https://www.cnblogs.com/rogerwu/p/10970848.html

你可能感兴趣的:(进击JavaScript核心 --- (3)面向对象)