【高级程序设计(第四版)】 - 对象、类与面向对象编程

229 阅读28分钟

理解对象

对象是一组属性的无序集合。可以将对象想象成一张散列表,其中的内容就是一组键/值对,值可以是数据或者函数。

属性的类型

  • 数据属性
    数据属性包含一个保存数据值的位置,数据属性具有4个特性描述它们的行为
    • [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为访问器属性。默认为true
    • [[Enumerable]]: 表示属性是否可以通过for-in循环返回(是否可枚举)。默认为true
    • [[Writable]]: 表示属性的值是否可被修改。默认为true
    • [[Value]]: 包含属性实际的值。默认为undefined只声明未赋值的变量被默认为undefined的来源) 想要修改属性的默认特性,就必须使用Object.defineProperty()方法,这个方法接收三个参数:对象本身、属性名称和一个描述符对象。描述符对象上的属性可以包含: configurableenumerablewritablevalue,跟相关特性的名称一一对应。想要修改对象特性,则可直接通过设置其中对应的属性。在调用 Object.defineProperty()时,configurableenumerablewritable 的值如果不指定,则都默认为 false
let dataProperties = {
  year: 2021,
  getName: () => {
    return 'name';
  }
};
Object.defineProperty(dataProperties, 'getName', {
  value: '通过方法修改'
});
// newProperty的数据属性:configurable、enumerable、writable均默认为false
Object.defineProperty(dataProperties, 'newProperty', {
  value: '只指定value'
});
// 读取属性的特性
const nameDescriptor = Object.getOwnPropertyDescriptor(dataProperties, 'getName');
const newDescriptor = Object.getOwnPropertyDescriptor(dataProperties, 'newProperty');
console.log('nameDescriptor', nameDescriptor);
console.log('newDescriptor', newDescriptor);
// 当前属性的数据属性writable为false,此不可被再次修改,修改无效。严格模式下报错
dataProperties.newProperty = '再改改';
console.log(dataProperties.newProperty); // '只指定value'

Object.defineProperty()

  • 访问器属性
    访问器属性不包含数据值。相反,它们包含一个获取(getter)函数和一个设置(setter)属性,但这两个函数不是必需的。在读取访问器属性时,会调用获取函数,在写入访问器属性时,会调用设置函数 并传入新值。 访问器属性有4个特性描述它们的行为。
    • [[Configurable]]:表示属性是否可以通过delete删除并重新定义,是否可以修改它的特性,以及是否可以把它改为数据属性。默认为true
    • [[Enumerable]]: 表示属性是否可以通过for-in循环返回(是否可枚举)。默认为true
    • [[Get]]: 获取函数,在读取属性时调用。默认为undefined只声明未赋值的变量被默认为undefined的来源
    • [[Set]]: 设置函数,在写入属性时调用(默认传入为新值)。默认值为undefined 访问器属性不能直接定义,必须使用Object.defineProperty()
 let book = {
  __year: 2021,
  edition: 1
};

Object.defineProperty(book, 'year', {
  get () {
    return this.__year;
  },
  set (newValue) {
    if (newValue > 2020) {
      this.__year = newValue;
      this.edition += newValue - 2021;
    }
  }
});

book.year = 2022;
console.log(book.edition); // 2
let yearDescriptor = Object.getOwnPropertyDescriptor(book, 'year');
// 使用Object.defineProperty()定义的访问器属性 configurable、enumerable默认为false
console.log('yearDescriptor', yearDescriptor); // { configurable:false, enumerable: false, get: f(), set: f(newValue) }

定义多个属性

  • Object.defineProperty(obj, property, desciptor)只能在一个对象上顶一个一个属性。
  • Object.definproperties(obj, desciptor)方法可以同时在一个对象上定义多个属性。接收2个参数:要定义的对象和描述符对象,描述符对象上的属性可以包含: configurableenumerablewritablevalue,跟相关特性的名称一一对应。两个方法的使用方式基本一致
let book = {}; 
Object.defineProperties(book, { 
 year_: { 
 	value: 2017 
 }, 
 edition: { 
 	value: 1 
 }, 
 year: {
   get() { 
   	return this.year_; 
   }, 
   set(newValue) { 
     if (newValue > 2017) { 
       this.year_ = newValue; 
       this.edition += newValue - 2017; 
     } 
   } 
 } 
});

读取属性的特性

  • Object.getOwnPropertyDescriptor(obj, property)   使用Object.getOwnPropertyDescriptor()方法可以取得指定属性的属性描述符(ts装饰器中的descriptor的获取来源方法?)。这个方法接 收两个参数:属性所在的对象和要取得其描述符的属性名。返回值是一个对象,对于访问器属性包含configurableenumerablegetset 属性,对于数据属性包含 configurableenumerablewritablevalue 属性
  • Object.getOwnPropertyDescriptors(obj)   读取对象上所有的自有属性的属性描述符。

合并对象

  具体一点描述,合并对象就是把源对象所有的本地属性一起㢟到目标对象上。EMAScript6专门提供了一个合并对象的方法Object.assign()

  • Object.assign(target, ...sources)(浅复制)   Object.assign()接收两个参数:目标对象、多个源对象。Object.assign()方法用于将所有可枚举属性(Object.propertyIsEnumerable()返回 true)和自有属性(Object.hasOwnProperty()返回 true)的值从一个或多个源对象分配到目标对象。它将返回目标对象。对每个符合条件的属性,这个方法会使用源对象上的[[Get]]取得属性的值,然后使用目标对象上的[[Set]]设置属性的值。
let targetObj = {
  year: 2021,
  age: 18
};

let sourceObj1 = {
  name: 'sourceObj1',
  age: 19
};

let sourceObj2 = {
  name: 'sourceObj2'
}
let resultObj = Object.assign(targetObj, sourceObj1, sourceObj2);
console.log('targetObj', targetObj);
console.log('resultObj', resultObj);

增强的对象语法

  • 属性简写   当属性名和变量名一致时,可简写。简写属性名只需要使用变量名即可被自动解释为同名的属性键。若没有找到同名变量,则抛出ReferenceError
  • 可计算属性   对象字面量中完成动态属性赋值,中括号包围的对象属性键可以使用js变量,运行时将其作为JavaScript表达式而不是字符串求值
  • 简写方法名   在给对象定义方法时,通常都要写一个方法名、冒号,然后再引用一个匿名函数表达式,现在可以直接使用函数声明方式
// 属性简写
let simple = '属性简写';
let obj = {
	simple
};
console.log(obj.simple); // '属性简写'
// 可计算属性
let key = 'simple';
console.log(obj[key]); // '属性简写'
// 简写方法
/* 原始写法 */
let person = {
	sayName: function (name) {
    	console.log(`My name is ${name}`);
    }
}
person.sayName('Nicholas');  // My name is Nicholas
/* 简写 */
let person1 = {
	sayName (name) {
    	console.log(`My name is ${name}`);
    }
}
person1.sayName('Nicholas'); // My name is Nicholas

对象解构

  解构赋值语法:可以在一条语句中使用嵌套数据实现一个或多个赋值操作。简单而言,对象结构就是使用与对象匹配的结构来实现对象属性赋值。解构赋值可以忽略某些属性;解构赋值还可以定义默认值, 若解构的属性在对象中不存在,则默认值为undefined

// 忽略属性
let person = {
	name: '焦糖瓜子',
    age: 18
};
let { name: personName, age: personAge } = person;
console.log(personName); // 焦糖瓜子
console.log(personAge); // 18
// 定义默认值
let { name, age, sex = 'female', job} = person;
console.log(name, age, sex, job); // 焦糖瓜子 18 female undefined

  解构在内部使用函数 ToObject()(不能在运行时环境中直接访问)把源数据结构转换为对象。这意味着在对象解构的上下文中,原始值会被当成对象。这也意味着(根据 ToObject()的定义),nullundefined 不能被解构,否则会抛出错误

  • 嵌套解构
let person = { 
 name: 'Matt', 
 age: 27, 
 job: { 
 title: 'Software engineer' 
 } 
}; 
let personCopy = {}; 
({ 
 name: personCopy.name, 
 age: personCopy.age, 
 job: personCopy.job 
} = person); 
// 因为一个对象的引用被赋值给 personCopy,所以修改person.job 对象的属性也会影响 personCopy 
person.job.title = 'Hacker'
  • 部分解构   涉及多个属性的解构赋值是一个输出无关的顺序化操作。如果一个解构表达式涉及 多个赋值,开始的赋值成功而后面的赋值出错,则整个解构赋值只会完成一部分
let person = { 
 name: 'Matt', 
 age: 27 
}; 
let personName, personBar, personAge; 
try { 
 // person.foo 是 undefined,因此会抛出错误
 ({name: personName, foo: { bar: personBar }, age: personAge} = person); 
} catch(e) {} 
console.log(personName, personBar, personAge); 
// Matt, undefined, undefined
  • 参数上下文   在函数参数列表中也可以进行解构赋值。对参数的解构赋值不会影响 arguments 对象,但可以在函数签名中声明在函数体内使用局部变量:
let person = { 
 name: 'Matt', 
 age: 27 
}; 
function printPerson(foo, {name, age}, bar) { 
 console.log(arguments); 
 console.log(name, age); 
} 
function printPerson2(foo, {name: personName, age: personAge}, bar) { 
 console.log(arguments); 
 console.log(personName, personAge); 
} 
printPerson('1st', person, '2nd'); 
// ['1st', { name: 'Matt', age: 27 }, '2nd'] 
// 'Matt', 27 
printPerson2('1st', person, '2nd'); 
// ['1st', { name: 'Matt', age: 27 }, '2nd'] 
// 'Matt', 27
  • 展开运算符
let arr = [
  {
    name: 'peron1'
  }, {
    person: 'person'
  },{
    age: 19
  }
];

const [ arr1, ...arr2 ] = arr;
console.log(arr1); // { name: 'person1' }
console.log(arr2); // [{ person1: 'person' }, { age: 19 }]

创建对象

设计模式产生的原因: 使用同一个接口创建很多对象,会产生大量的重复代码。

工厂模式

工厂模式的缺点: 工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)

createPerson()能够根据接受的参数来构建一个包含所有必要信息的 Person 对象。可以无数次地调用这个函数

function createPerson (name, age, job) {
    let o = new Object();
    o.name = name;
    o.job = job;
    o.age = age;
    o.say = function () {
       return name;
    }
    return o;
}
var person1 = createPerson("Nicholas", 29, "Software Engineer"); 
var person2 = createPerson("Greg", 27, "Doctor");
console.log(person1); // {name: "Nicholas", job: "Software Engineer", age: 29, say: ƒ}
person1.name = '焦糖瓜子';
console.log(person2); // {name: "焦糖瓜子", job: "Software Engineer", age: 29, say: ƒ}

构造函数模式

构造函数模式存在的问题: 使用构造函数时,每次每个方法都会在实例中重新创建一次,每个实例的中方法并不是同一个方法 (function也是引用类型,每次创建都是实例化了一个对象)。 person1 和 person2 都有一个名为 sayName()的方法,但那两个方法不是同一个 Function 的实例

构造函数能够进行创建对象主要依靠 new 操作符

function Person (name, age, job) {
    this.name = name;
    this.age = age;
    this.job = job;
    this.say =  function () {
        return name;
    }
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
console.log(person1.say === person2.say); // false
console.log(person1); // Person {name: "Nicholas", age: 29, job: "Software Engineer", say: ƒ}

原型模式

原型模式存在的问题: 原型模式中所有的实例都是共用构造函数中的原型对象,当所有实例中某个实例将原型对象中的引用类型的值进行修改时,其他实例对象中相对的属性值(当前属性未被实例对象覆盖)也会被进行修改

我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象, 而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法

  • 原型对象添加属性方法
// 构造函数 Person
function Person () {}
// 原型对象添加属性和方法
Person.prototype.name = 'Nicholos';
Person.prototype.age = '24';
Person.prototype.job = 'Software Engineer';
Person.prototype.friends = ["Shelby", "Court"],
Person.prototype.say = function () {
    console.log('Prototype Function');
}
var person1 = new Person();
var person2 = new Person();
console.log(person1.say === person2.say);  // true
console.log(person1.friends); // ["Shelby", "Court"]
// 其中一个实例将原型对象中的引用类型值进行修改,其他实例也将受到影响
person2.friends.push('焦糖瓜子'); 
console.log(person1.friends); //  ["Shelby", "Court", "焦糖瓜子"]
// 实例中创建与原型对象同名的属性(基本类型),会将原型链的属性进行屏蔽
person1.name = '焦糖瓜子';
console.log(person1.name); // 焦糖瓜子
console.log(person2.name); // Nicholos
  • 使用对象字面量
    Person.prototype设置为等于一个以对象字面量形式创建的新对象。最终结果相同,但有一个例外:constructor属性不再指向Person了,每创建一个函数,就会同时创建它的 prototype 对象,这个对象也会自动获得constructor 属性。字面量方式的本质上完全重写了默认的prototype对象(引用类型),因此constructor属性也就变成了新对象的 constructor属性(指向 Object 构造函数),不再指向Person函数
function Person(){} 
Person.prototype = {
    name : "Nicholas", 
    age : 29, 
    job: "Software Engineer", 
    sayName : function () { 
        console.log('Prototype Function');
    }
};
// 重设构造函数: 如果constructor比较重要可将constructor重新指向构造函数
Object.defineProperty(Person.prototype, "constructor", { 
    enumerable: false, 
    value: Person 
});
var person1 = new Person(); 
console.log(person1 instanceof Object); // true 
console.log(person1 instanceof Person); // true 
console.log(person1.constructor == Person); // false 
console.log(person1.constructor == Object); // true
  • 原型的动态性

其原因可以归结为实例与原型之间的松散连接关系,因为实例与原型之间的连接只不过是一个指针,而非一个副本。 请记住:实例中的指针仅指向原型,而不指向构造函数。

调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。

function Parent(){} 
// friend的__proto__仍旧为默认的prototype的指针, 指向原来的堆内存
var friend = new Parent(); 
// 重新原型相当于在堆内存中新建一个对象,将Parent的prototype指针修改为新对象堆内存
Parent.prototype = { 
    name : "Nicholas", 
    age : 29, 
    job: "Software Engineer", 
    sayName : function () { 
        console.log('Prototype Function');
    } 
};
console.log(friend);
console.log(Parent);
console.log(friend.name); // undefiend 

原型动态性

组合使用构造函数模式和原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参数;可谓是集两种模式之长

function Person(name, age, job){ 
    this.name = name; 
    this.age = age; 
    this.job = job; 
    this.friends = ["Shelby", "Court"]; 
} 
Person.prototype = { 
    constructor : Person, 
    sayName : function(){ 
        console.log(this.name); 
    } 
} 
var person1 = new Person("Nicholas", 29, "Software Engineer"); 
var person2 = new Person("Greg", 27, "Doctor"); 
person1.friends.push("Van"); 
console.log(person1.friends); //"Shelby,Count,Van" 
console.log(person2.friends); //"Shelby,Count" 
console.log(person1.friends === person2.friends); //false 
console.log(person1.sayName === person2.sayName); //true

寄生构造函数模式

寄生构造函数模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但 从表面上看,这个函数又很像是典型的构造函数

function Person(name, age, job){ 
    // 创建一个新对象,并以相应的属性和方法初始化该对象
    var o = new Object(); 
    o.name = name; 
    o.age = age; 
    o.job = job; 
    o.sayName = function(){ 
        alert(this.name); 
    }; 
    return o; 
} 
var friend = new Person("Nicholas", 29, "Software Engineer"); 
friend.sayName(); //"Nicholas"

  关于寄生构造函数模式,有一点需要说明:首先,返回的对象与构造函数或者与构造函数的原型属性之间没有关系;也就是说,构造函数返回的对象与在构造函数外部创建的对象没有什么不同。为此,不能依赖 instanceof 操作符来确定对象类型。

继承

原型链继承

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

原型链继承的缺点

  • 原型对象中的引用类型会被所有的实例共享,某实例修改原型对象中的引用类型会导致其他实例中该属性 、均被修改
  • 在创建子类型的实例时,不能向超类型的构造函数中传递参数
// 原型链继承
function SuperType () {
    this.name = 'Parent';
    this.color = ['red', 'blue', 'orange'];
}
SuperType.prototype.getName = function () {
    console.log(this.name);
}
function SubType () {}
// SubType的原型对象的constructor不再指向SubType,而是指向了SuperType,此处相当于重写了SubType.prototype
SubType.prototype = new SuperType();

// SubType中重写SuperType的属性与方法需要注意:
// 1、重写SuperType中的方法与属性,一定要在实例化SuperType之后进行重写,prototype关联后会将prototype指向超类的原型对象
// 2、不要使用字面量的方式重写SuperType中的方法与属性。因为这样会重写原型链,导致SubType.protutype不再关联SuperType
SubType.prototype.name = 'child';
var instance = new SubType();

// getName中的this指向为instance,SuperType中name属性被SubType进行了覆盖
instance.getName(); // child
console.log(instance instanceof SubType); // true
console.log(instance instanceof SuperType); // true

// 原型链继承的问题: 
// 1、原型对象中的引用类型会被所有的实例共享,某实例修改原型对象中的引用类型会导致其他实例中该属性 、均被修改
// 2、在创建子类型的实例时,不能向超类型的构造函数中传递参数
instance.color.push('black');
console.log(instance.color);  // ['red', 'blue', 'orange', 'black']
var instance1 = new SubType();
console.log(instance1.color); // ['red', 'blue', 'orange', 'black']

构造函数

重写超类中的属性与方法注意点

  • 重写超类中的方法与属性,一定要在超类实例化后进行重写,实例的prototype指向另外的对象(超类的prototype),实例化之前的属性与方法将会丢失(引用类型地址重写
  • 不要使用字面量的方式重写超类中的方法与属性。字面量方式会重写原型链,导致子类的prototype不再关联超类

构造函数继承

为解决原型中包含引用类型值所带来问题的过程中,开发人员开始使用一种叫做借用构造函数的技术(有时候也叫做伪造对象或经典继承)。这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数

function SuperType (name) {
    this.colors = ['red', 'blue', 'orange'];
    this.name = name;
}
// 超类原型对象中创建的方法与属性对子类不可见
SuperType.prototype.sex = 'female';
SuperType.prototype.getName = function() {
    console.log(this.name);
}
function SubType () {
    // 构造函数中调用超类构造函数
    SuperType.call(this, 'subType构造函数传入的参数name');
    this.age = 23;
}
var instance = new SubType();
instance.colors.push('black');
console.log(instance.colors); // ["red", "blue", "orange", "black"]
console.log(instance.name); // subType构造函数传入的参数name
console.log(instance.female); // undefined
// instance.getName(); // typeError: instance.getName is not a function
var instance1 = new SubType();
console.log(instance1.colors); // ["red", "blue", "orange"]
console.log(instance1.name); // subType构造函数传入的参数name

构造函数继承的缺点

  • 在超类型的原型中定义的方法,对子类型而言也是不可见的

组合继承(原型链 + 借用构造函数)

组合继承是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性

function SuperType (name) {
    this.colors = ['red', 'blue', 'orange'];
    this.name = name;
}
SuperType.prototype.getName = function() {
    console.log(this.name);
}
function SubType (name, age) {
    // 继承属性  实质为:在子类中重新定义了父类的属性,进行了覆盖父类属性
    SuperType.call(this, name);
    this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
// 重新绑定constructor, 继承方法时 prototype原型对象被重写,constructor指向了SuperType
SubType.prototype.constructor = SubType;

var instance = new SubType('subType构造函数传入的参数name', 50);
instance.getName(); // subType构造函数传入的参数name
instance.colors.push('black');
console.log(instance.colors); // ["red", "blue", "orange", "black"]

var instance1 = new SubType('subType构造函数传入的参数name111', 20);
console.log(instance1.colors); // ["red", "blue", "orange"]
console.log(instance); // 下图所示

组合继承

组合继承的缺点

  • 组合继承都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部,子类、构造函数都会创建超类型构造函数中的属性

原型式继承

借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型

在inheritObject()函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上讲,inheritObject()(原型式继承)对传入其中的对象执行了一次浅复制

function inheritObject (o) {
    function F() {};
    F.prototype = o;
    return new F();
}
var person = { 
    name: "Nicholas", 
    friends: ["Shelby", "Court", "Van"] 
}; 
var anotherPerson = inheritObject(person); 
anotherPerson.name = "Greg"; 
anotherPerson.friends.push("Rob"); 
// ECMAScript 5 通过新增 Object.create()方法规范化了原型式继承。
// 这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda"; 
yetAnotherPerson.friends.push("Barbie"); 
console.log(person.friends); // ["Shelby", "Court", "Van", "Rob", "Barbie"]

如果只想让一个对象与另一个对象保持类似的情况下使用原型式继承即可,不需要创建复杂的构造函数。

原型式继承的缺点

  • 原型式继承与原型链继承存在同样的缺点,包含引用类型值的属性始终都会共享相应的值

寄生式继承

寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象

function inheritObject (o) { 
    function F() {};
    F.prototype = o;
    return new F();
}
function createAnother(original){ 
    var clone = inheritObject(original); //通过调用函数创建一个新对象
        clone.sayHi = function(){ //以某种方式来增强这个对象
        console.log("hi"); 
    }; 
    return clone; //返回这个对象
}
var person = { 
    name: "Nicholas", 
    friends: ["Shelby", "Court", "Van"] 
}; 
var anotherPerson = createAnother(person); 
anotherPerson.sayHi(); //"hi

寄生式继承的缺点

  • 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似

寄生组合式继承

基本思路是:不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型

寄生组合式继承的优点

  • 寄生组合式继承的高效率体现在它只调用了一次超类SuperType构造函数,并且因此避免了在构造函数原型对象SubType.prototype上面创建不必要的、多余的属性。与此同时,原型链还能保持不变
function inheritObject (o) {
    function F() {};
    F.prototype = o;
    return new F();
}
// 寄生组合式继承的简单形式
function inheritPrototype(subType, superType) {
    // 复制超类的原型对象
    var prototypeObj = inheritObject(superType.prototype);
    // 修正constructor指向
    prototypeObj.constructor = subType;
    // 原型链继承:修正构造函数的prototype
    subType.prototype = prototypeObj;
}

function SuperType (name) {
    this.colors = ['red', 'blue', 'orange'];
    this.name = name;
}

SuperType.prototype.getName = function() {
    console.log(this.name);
}

function SubType (name, age) {
    SuperType.call(this, name);
    this.age = age;
}
// SubType复制为SuperType的原型对象
inheritPrototype(SubType, SuperType);
var instance = new SubType('Nichloas', 29);
instance.colors.push('black'); // 
console.log(instance.colors); // ["red", "blue", "orange", "black"]
var instance1 = new SubType('Nichloas', 29);
console.log(instance1.colors); // ["red", "blue", "orange"]
console.log(instance); // 下图显示

instance

超类、构造函数 - 原型对象链接

原型链

原型链

重点

五条原型规则

  • 所有的引用类型(数组、对象、函数)都具有对象特性,即可自由扩展属性(除了"null")
  • 所有的引用类型(数组、对象、函数)都有一个__proto_(隐式原型)_属性,属性值是一个普通的对象
  • 所有的函数(只有函数才具有显示原型),都有一个prototype(显示原型)属性,属性值也是一个普通的对象
  • 所有的引用类型(数组、对象、函数), __proto__(隐式原型)属性值 指向它构造函数的prototype(显示原型)属性值
  • 当视图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数prototype)中寻找

普通对象和函数对象

JavaScript 中,万物皆对象!但对象也是有区别的。分为普通对象和函数对象,Object 、Function 是 JS 自带的函数对象。

var o1 = {}; 
var o2 =new Object();
var o3 = new f1();

function f1(){}; 
var f2 = function(){};
var f3 = new Function('str','console.log(str)');

console.log(typeof Object); //function 
console.log(typeof Function); //function  

console.log(typeof f1); //function 
console.log(typeof f2); //function 
console.log(typeof f3); //function   

console.log(typeof o1); //object 
console.log(typeof o2); //object 
console.log(typeof o3); //object

  在上面的例子中 o1 o2 o3 为普通对象,f1 f2 f3 为函数对象。怎么区分,其实很简单,凡是通过 new Function() 创建的对象都是函数对象,其他的都是普通对象。f1,f2,归根结底都是通过 new Function()的方式进行创建的。Function Object 也都是通过 New Function()创建的。

原型模式

理解原型

  无论何时,只要创建一个函数,就会按照特定规则为函数创造一个prototype属性(指向原型对象)。默认情况下,所有的原型对象自动获得一个名为constructor的属性。指回与之相关的构造函数。

原型对象

  所有的原型对象(Person.prototype)自动获得一个名为constructor的属性。指回与之相关的构造函数(Person)。原型对象其实就是普通对象(但 Function.prototype 除外,它是函数对象,但它很特殊,他没有prototype属性)

function Person() {}
Person.prototype.name = 'Zaxlct';
Person.prototype.age  = 28;
Person.prototype.job  = 'Software Engineer';
Person.prototype.sayName = function() {
  alert(this.name);
}

console.log(Person.prototype); // {name: xxxx,constructor: Person,__proto__: Object}
console.log(Person.prototype.constructor === Person); // true
__proto __  

  JS在创建对象(不论是普通对象还是函数对象)的时候,都有一个叫做__proto__(隐式原型) 的内置属性,用于指向创建它的构造函数的原型对象。

function Person() {}
Person.prototype.name = 'Zaxlct';
Person.prototype.age  = 28;
Person.prototype.job  = 'Software Engineer';
Person.prototype.sayName = function() {
  alert(this.name);
}

// new 操作符会将构造函数自身的属性和方法也赋值给实例对象
let person = new Person();
console.log(person.__proto__ === Person.prototype); // true
构造函数

  在自定义狗仔函数时,原型对象只会默认获得constructor属性,其他方法都继承自Object。每次使用构造函数创建一个新实例,这个实例内部[[prototype]]指针(即__proto__)会被赋值为构造函数的原型对象(fn.prototype)。实例与构造函数原型之间有直接联系,但实例与构造函数之间没有直接的联系 如图: 原型链

/** 
 * 构造函数可以是函数表达式
 * 也可以是函数声明,因此以下两种形式都可以:
 * function Person() {} 
 * let Person = function() {} 
 */
 function Person() {}
/** 
 * 声明之后,构造函数就有了一个
 * 与之关联的原型对象 普通对象: Person.prototype,
 */
 console.log(typeof Person.prototype); // 'object'
 console.log(Person.prototype); // { constructor: Person, __proto__: Object }
 /** 
 * 如前所述,构造函数有一个 prototype 属性
 * 引用其原型对象,而这个原型对象也有一个
 * constructor 属性,引用这个构造函数
 * 换句话说,两者循环引用:
 */
 console.log(Person.prototype.constructor === Person); // true
 /** 
 * 正常的原型链都会终止于 Object 的原型对象
 * Object 原型的原型是 null 
 */
 console.log(Person.prototype.__proto__ ===   Object.prototype); // true
 console.log(Person.prototype.__proto__.constructor === Object); // true
 console.log(Person.prototype.__proto__.__proto__ === null); // true

 /** 创建新实例 */
 let person = new Person();
 let person1 = new Person();
 let person2 = new Person();
 /** 
 * 构造函数、原型对象和实例
 * 是 3 个完全不同的对象:
 */
 console.log(person !== Person); // true
 console.log(person !== Person.prototype); // true
 console.log(Person.prototype !== Person); // true
 /** 
 * 实例通过__proto__链接到原型对象,
 * 它实际上指向隐藏特性[[Prototype]] 
 * 
 * 构造函数通过 prototype 属性链接到原型对象
 * 
 * 实例与构造函数没有直接联系,与原型对象有直接联系
 */
 console.log(person1.__proto__ === Person.prototype); // true 
 console.log(person1.__proto__.constructor === Person); // true

 /** 
 * 同一个构造函数创建的两个实例
 * 共享同一个原型对象:
 */ 
console.log(person1.__proto__ === person2.__proto__); // true

/** 
 * instanceof 检查实例的原型链中
* 是否包含指定构造函数的原型:
 */ 
console.log(person1 instanceof Person); // true 
console.log(person1 instanceof Object); // true 
console.log(Person.prototype instanceof Object); // true

原型对象之间的关系

原型层级

  在实例中访问某个属性,若该实例自身没有该属性,则该实例会将请求委托给构造函数的原型对象,一级一级进行查到,知道找到或者找不到。实例重写原型上的属性或方法,会遮蔽原型对象的同名属性或方法。

function Person() {} 
Person.prototype.name = "Nicholas";
Person.prototype.age = 29; 
Person.prototype.job = "Software Engineer"; 
Person.prototype.sayName = function() { 
 console.log(this.name); 
}; 
let person1 = new Person(); 
let person2 = new Person(); 
console.log(person1.hasOwnProperty("name")); // false 

person1.name = "Greg"; 
console.log(person1.name); // "Greg",来自实例
console.log(person1.hasOwnProperty("name")); // true 

console.log(person2.name); // "Nicholas",来自原型
console.log(person2.hasOwnProperty("name")); // false 

delete person1.name; 
console.log(person1.name); // "Nicholas",来自原型
console.log(person1.hasOwnProperty("name")); // false

  通过调用 hasOwnProperty()能够清楚地看到访问的是实例属性还是原型属性。 调用 person1.hasOwnProperty("name")只在重写 person1 上 name 属性的情况下才返回 true,表明此时 name 是一个实例属性,不是原型属性。

其他原型语法

  前面所有的例子中,每次定义一个属性或方法都会把Person.prototype重写一次。为了减少代码冗余,也为了从视觉上更好地封装原型功能,可以直接通过一个包含所有属性和方法的对象字面量来重写原型。

function Person() {} 
Person.prototype = {
 name: "Nicholas", 
 age: 29, 
 job: "Software Engineer", 
 sayName() { 
   console.log(this.name); 
 } 
};
console.log(Person.prototype.constrcutor); // Object
let person = new Person();
console.log(person.constructor == Person); // false
console.log(person.constructor == Object); // true

  这种写法存在一个问题:重写了Person.prototype之后,原型对象只会默认获得constructor属性就不再指向Person而是Object',原型对象自由属性不再包含constructor。字面量方式需要显示指定constructor属性的值   修复constructor指向

// 方法一:存在问题: 自动生成的constructor属性[[Enumerable]]特性为false,但是字面方式恢复的constructor属性的[[Enumerable]]特性为true
function Person() { 
} 
Person.prototype = { 
 constructor: Person, 
 name: "Nicholas", 
 age: 29, 
 job: "Software Engineer", 
 sayName() { 
 	console.log(this.name); 
 } 
};
// 方法二
function Person() {} 
Person.prototype = { 
 name: "Nicholas", 
 age: 29, 
 job: "Software Engineer", 
 sayName() { 
 console.log(this.name); 
 } 
}; 
// 恢复 constructor 属性
Object.defineProperty(Person.prototype, "constructor", { 
 enumerable: false, 
 value: Person 
});
原型的动态性

其原因可以归结为实例与原型之间的松散连接关系,因为实例与原型之间的连接只不过是一个指针,而非一个副本。 请记住:实例中的指针仅指向原型,而不指向构造函数。   调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。

function Parent(){} 
// friend的__proto__仍旧为默认的prototype的指针, 指向原来的堆内存
var friend = new Parent(); 
// 重新原型相当于在堆内存中新建一个对象,将Parent的prototype指针修改为新对象堆内存
Parent.prototype = { 
    name : "Nicholas", 
    age : 29, 
    job: "Software Engineer", 
    sayName : function () { 
        console.log('Prototype Function');
    } 
};
console.log(friend);
console.log(Parent);
console.log(friend.name); // undefiend 

原型动态性

原型(原型链)的问题

  原型模式也不是没有问题。首先,它弱化了向构造函数传递初始化参数的能力,会导致所有实例默认都取得相同的属性值。虽然这会带来不便,但还不是原型的最大问题。原型的最主要问题源自它的共享特性。原型对象上的所有属性和方法在实例之间是共享的,原型对象上的引用值被一个实例修改了,也会体现在其他实例上

function Person() {} 
Person.prototype = { 
 constructor: Person, 
 name: "Nicholas", 
 age: 29, 
 job: "Software Engineer", 
 friends: ["Shelby", "Court"],
 sayName() { 
 	console.log(this.name); 
 } 
}; 
let person1 = new Person(); 
let person2 = new Person(); 
person1.friends.push("Van"); 
console.log(person1.friends); // "Shelby,Court,Van" 
console.log(person2.friends); // "Shelby,Court,Van" 
console.log(person1.friends === person2.friends); // true

原型链

ECMA-262 把原型链定义为 ECMAScript 的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法。原型链的基本构想:原型A对象本身有一个内部指针__proto__指向另一个原型对象B,相应地原型B也有一个指针__proto__指向另一个原型对象C。这样就在实例和原型之间构造了一条原型链。 默认情况下,所有引用类型都继承自 Object,这也是通过原型链实现的 原型链

原型相关方法

Object.create(proto,[propertiesObject])
  • proto: 新创建对象的原型对象
  • propertiesObject:可选, 需要传入一个对象,要定义其可枚举属性或修改的属性描述符的对象。对象中存在的属性描述符主要有两种:数据描述符访问器描述符, 即新对象的属性
  • 返回值: 一个新对象,带着指定的原型对象和属性。   Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。返回一个新对象,带着指定的原型对象和属性。
function Animal() {
  this.type = 'animal';
}
Animal.prototype.makeSound = function () {
  console.log('makeSound');
}

function Dog() {
  this.name = 'dog';
}
// 子类继承父类 - 相当于将Dog的原型对象重写,会导致Constructor也指向了Animal
Dog.prototype = Object.create(Animal.prototype);
console.log(Dog.prototype.constructor); // f Animal {}
// 修复constructor指向
Dog.prototype.constructor = Dog;
let dog = new Dog();
console.log('dog', dog);

// propertiesList
let o = {};
o = Object.create(Animal.prototype, {
  foo: {
    writable: true,
    configurable: true,
    value: 'fooo'
  }
});
console.log('o', o); // { foo: 'fooo', __proto__: { makeSound: f(), constructor: f Animal() } }
Object.getPrototypeOf(object)
  • 要返回其原型的对象
  • 返回值:给定对象的原型。如果没有继承属性,则返回null   Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)
var proto = {};
let result1 = Object.getPrototypeOf(proto);
console.log('result1', result1); // 表示Object.prototype原型对象
console.log('object', Object.prototype);
function Animal() {
  this.type = 'animal';
}
Animal.prototype.makeSound = function () {
  console.log('makeSound');
}
let result = Object.getPrototypeOf(Animal);
console.log('result', result); // 指向Function.prototype
console.log(Animal.__proto__.constructor); // f Function (){}
Object.setPrototypeOf(obj, prototype)
  • obj: 要设置其原型的对象
  • prototype: 该对象的新原型(一个对象 或 null).

由于现代 JavaScript 引擎优化属性访问所带来的特性的关系,更改对象的 [[Prototype]]在各个浏览器和 JavaScript 引擎上都是一个很慢的操作。其在更改继承的性能上的影响是微妙而又广泛的,这不仅仅限于 obj.proto = ... 语句上的时间花费,而且可能会延伸到任何代码,那些可以访问任何[[Prototype]]已被更改的对象的代码。如果你关心性能,你应该避免设置一个对象的 [[Prototype]]。相反,你应该使用 Object.create()来创建带有你想要的[[Prototype]]的新对象   不推荐使用,不进行测试

Object.getOwnPropertyNames(obj)
  • obj: 一个对象,其自身的可枚举和不可枚举属性的名称被返回。
  • 返回值:在给定对象上找到的自身属性对应的字符串数组。   Object.getOwnPropertyNames()方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组
let obj = {
  name: '焦糖瓜子',
  age: 18,
  sex: 'female'
};
Object.defineProperty(obj, 'job', {
  value: '前端搬砖工'
});
console.log('getOwnPropertyNames', Object.getOwnPropertyNames(obj)); // ["name", "age", "sex", "job"]
Object.prototype.hasOwnProperty(prop)
  • prop: 要检测的属性的 String 字符串形式表示的名称,或者 Symbol。   hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性(也就是,是否有指定的键)。原型对象上的属性和方法可直接使用 对象本身去获取和执行in关键字可遍历在原型链上的属性
function Animal() {
  this.type = 'animal';
}
Animal.prototype.makeSound = function () {
  console.log('makeSound');
}
const dog = new Animal();
dog.name = '小狗狗';
console.log('dog', dog);
console.log('hasOwnProperty', dog.hasOwnProperty('makeSound')); // false
console.log('hasOwnProperty', 'makeSound' in dog); // true
console.log('hasOwnProperty', dog.hasOwnProperty('name')); // true
Object.prototype.isPrototypeOf(object)
  • 语法:prototypeObj.isPrototypeOf(object)
  • 返回值:Boolean,表示调用对象是否在另一个对象的原型链上。   isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上

isPrototypeOf() 与 instanceof 运算符不同。在表达式 "object instanceof AFunction"中,object 的原型链是针对AFunction.prototype 进行检查的,而不是针对 AFunction 本身

function  Animal () {
  this.type = 'animal';
}

const dog = new Animal();
console.log('isPropertyOf', Animal.prototype.isPrototypeOf(dog)); // true
console.log('isPropertyOf', Function.prototype.isPrototypeOf(dog)); // false
console.log('isPropertyOf', Object.prototype.isPrototypeOf(dog)); // true
console.log('isPropertyOf', Function.prototype.isPrototypeOf(Animal)); // true
console.log('isPropertyOf', Object.prototype.isPrototypeOf(Function)); // true

console.log('instanceof', dog instanceof Animal); // true
console.log('instanceof', dog instanceof Object); // true
instanceof

f instanceof Foo的判断逻辑:
1、f 的__ptoto__一层一层往上,能否对应到Foo.prototype
2、再试着判断f instanceof Object   思维导图

类的定义

类构造函数

实例、原型和类成员

继承