欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

JavaScript高程第六章:继承-理解与实践

程序员文章站 2022-06-12 20:45:01
...

JavaScript高程第六章:继承-理解与实践
昨日细细的读了一遍JavaScript高程,现在写篇文章来巩固下认知吧.

首先是从中读到了什么,我自己也在读书的时候用笔记下了各个部分的点,现在等于阅读笔记回忆下书本.

理解基础

ECMA-262(第五版)
ECMA中规定了两种属性:数据属性 and 访问器属性

数据属性

包含一个数据值的位置(读取和写入)
4个描述行为的特性

  • [[Configurable]] 默认值为true,描述了可否delete,可否修改其特性(变更为访问器属性)

  • [[Enumerable]] 默认值为true,描述了能否通过for-in循环返回属性.

  • [[Writable]] 默认为true,能否修改属性的值

  • [[Value]] 默认为undefined,就是属性的值

相关函数 Object.defineProperty(属性所在对象,属性名,描述符对象(可多个,{}))
注!修改configurable为false,则对后续调用该方法有限制,变得只能修改Writable和Value特性.

访问器属性

不包含属性值,包含一对getter和setter函数(非必需),同样有4个特性,相同功能不多加解释.

  • [[Configurable]] 默认值为true

  • [[Enumerable]] 默认值为true

  • [[Get]] default:undefined getter函数

  • [[Set]] default:undefined setter函数
    注!访问器属性不能直接定义,必须使用Object.defineProperty()定义,在严格模式中,尝试写入只指定了getter函数的属性会抛出错误,尝试读取只指定了setter函数的属性同理.

非严格模式中,则会忽略/返回undefined

相关函数和兼容

Object.defineProperty(属性所在对象,属性名,描述符对象(可多个,{}))
支持:IE9+(IE8部分实现),Firefox4+,Safari5+,Opera 12+和Chrome
不兼容解决方案:__defineGetter__(属性名,函数),__defineSetter__(属性名,函数)
但是无法解决对[[Configurable]]和[[Enumerable]]的修改

Object.defineProperties(对象,{属性1:{描述符},属性2:{}...})
支持:IE9+(IE8部分实现),Firefox4+,Safari5+,Opera 12+和Chrome

Object.getOwnPropertyDescriptor(对象,属性名)
返回:对象(访问器/格式)
可以对JS中任何对象,包括BOM,DOM使用.

创建对象

  1. 工厂模式

  2. 构造函数模式

  3. 原型模式 - 引申出原型对象的理解

  4. 组合模式 解决原型模式问题

  5. 动态原型模式

  6. 寄生构造函数模式

  7. 稳妥构造函数模式

工厂模式

缺点:未解决识别问题(怎么知道一个对象的类型)

示例:

function makePerson(name,age,job){
    var o  =new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.arr = ["a","b"];
    o.sayName = function(){
        alert(this.name);
    }
    return o;
}
var a = makePerson("jack",18,"programmer");
var b = makePerson("james",20,"designer");
a.arr.push("c");
console.log("a:"+a.arr); //a:a,b,c
console.log("b:"+b.arr); //b:a,b
console.log(a instanceof makePerson);//false
console.log(b instanceof makePerson);//false
console.log(a.prototype); //undefined
console.log(b.prototype); //undefined
console.log(a.prototype); //undefined
console.log(b.prototype); //undefined

构造函数模式

应该值得注意的是构造函数我们是大写字母开头,这是约定俗成的.创建一个Person示例我们会有如下步骤.

  1. 创建一个新对象

  2. 将构造函数作用域赋给新对象(this指向)

  3. 执行构造函数中的代码(为新对象添加属性)

  4. 返回新对象
    instanceof操作符和constructor属性都能让我们分辨出这是一种特定的类型,这也是构造函数模式胜过工厂模式的地方.

如果直接作为普通函数调用,则会将属性赋值给window对象(Global)

问题:函数不复用问题,实例中的方法不是同一个Function的实例,鉴定方法.
console.log(a.sayName == b.sayName)
解决:放到全局定义,构造函数中设置即可
导致新问题:毫无封装性,而为了解决这些问题,我们可以使用后续的原型模式来解决.

注!所有对象都继承自Object,所以a,b使用instanceof操作符判断是否为Object的实例是true.

示例:

function Person(name,age,job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.arr = ["a","b"];
  this.sayName = function(){
    alert(this.name);
  };
}
var a = new Person("jack",18,"programmer");
var b = new Person("james",20,"designer");
a.arr.push('c');
console.log("a:"+a.arr); //a:a,b,c
console.log("b:"+b.arr); //b:a,b
console.log(a instanceof Person);//true
console.log(b instanceof Person);//true
console.log(a.prototype); //undefined
console.log(b.prototype); //undefined
console.log(a.constructor); //[Function: Person]
console.log(b.constructor); //[Function: Person]

原型模式

每一个function都有一个prototype(原型)属性,为一个指针,指向一个对象(用途:包含可以由特定类型的所有实例共享的属性和方法).

通过prototype设置的属性和方法都是共享的,接下来让我们理解一下原型对象.

理解原型对象

在任何时候,我们创建一个新函数都意味着我们会根据一个特定规则创建prototype属性,该属性指向函数的原型对象.
在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这个属性包含一个指向prototype属性所在函数的指针.
调用构造函数创建一个实例后,实例内部包含一个指针[[Prototype]] (Firefox,Safari,Chrome访问使用__proto__),
对于判断可以使用Person.prototype.isPrototypeOf(a)函数.Person的prototype为a的prototype
Object.getPrototypeOf可以访问[[Prototype]]的值.

值得注意的是,我们可以通过对象实例来访问保存在原型的值,但是我们不能通过对象实例重写原型的值(对象.属性 = 值,这样是添加属性到实例,覆盖屏蔽了原型的值而已,并没有重写,但是对于引用类型不同,即使设置对象.属性=null也是不会恢复其指向,只是在实例中写入属性.对象为null而已,要想恢复,可以使用delete操作符)

原型与in操作符

方式一:for-in循环中使用
方式二:单独使用,会在能访问(不管通过对象还是原型)给定属性时返回true(所有能通过对象访问,可枚举的属性)
所有开发人员定义的属性都是可枚举的(IE8以及更早例外,其中屏蔽的不可枚举属性的实例属性不会出现在for-in循环中)

相关函数:
a.hasOwnProperty(属性名),可以确定属性是否存在于实例中,是则返回true
var keys = Object.keys(Person.prototype)
变量中保存一个数组,Object.keys返回的是一个包含所有可枚举属性的字符串数组.
Object.getWenPropertyNames()可以获取所有实例属性(无论是否可枚举)

更简单的原型语法

Person.prototype = {
    name : "Nicholas",
    age: 29,
    job: "software engineer",
    sayName:fuinction(){
        alert("this.name");
    }
}

在上面代码中,我们相当于完全重写了prototype对象,同时其constructor不再指向Person(指向Object构造函数),尽管instanceof操作符能返回正确结果,但是constructor已经无法确定对象类型了.当然我们可以自己在新建对象时候设置constructor: Person,但是这样做会导致它变为可枚举属性(原生不可枚举,解决方法:Object.defineProperty()).

原型的动态性

使用上述原型语法,会切断构造函数与最初原型的联系.
var friend = new Person()出现在完全重写之前,则我们无法通过friend访问重写的原型.

function Person(){

}
var friend = new Person();
Person.prototype = {
  constructor: Person,
  name: "Jack",
  age: 29,
  job: "programmer",
  sayName:function(){
    console.log(this.name);
  }
}
console.log(friend.age); // undefined
friend.sayName();  //报错

friend中的[[Prototype]]指向的仍然是原来的空无一物的Prototype,而不是我们后来重写的原型对象.

原生对象的原型

原生引用类型(Object,Array,String等)都采用原型模式创建
注!不推荐修改原生对象的原型,可能导致命名冲突/重写原生方法.

原型对象的问题

共享引用类型值的属性,如Array,修改则会共享

示例:

function Person(){
}
Person.prototype.name = "Jack";
Person.prototype.age = 18;
Person.prototype.job = "Software Engineer";
Person.prototype.arr = ["a","b"];//引用类型
Person.prototype.sayName = function(){
  console.log(this.name);
}

var a = new Person();
a.sayName(); //Jack
var b = new Person();
b.name = "James";//创建值,屏蔽了原型的值

console.log(b.age);//18
console.log(b);//Person { name: 'James' }

b.sayName();//James

console.log(a.sayName == b.sayName);//true
a.arr.push("c");//修改引用类型
console.log("a:"+a.arr); //a:a,b,c
console.log("b:"+b.arr); //b:a,b,c
console.log(a instanceof Person);//true
console.log(b instanceof Person);//true
console.log(a.prototype); //undefined
console.log(b.prototype); //undefined
console.log(a.constructor); //[Function: Person]
console.log(b.constructor); //[Function: Person]

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

解决原型模式的问题-共享引用类型值的属性
其中特点在于,实例属性在构造函数中定义,共享的constructor与方法在原型中定义,如下.

目前来说最广泛,认同度最高的一种方式来创建自定义类型.

示例:

function Person(name,age,job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ["a", "b"];
}

Person.prototype = {
  constructor:Person,
  sayName:function(){
    console.log(this.name);
  }
}
var a = new Person("jack",18,"programmer");
var b = new Person("james",20,"designer");

a.friends.push('c');
console.log(a.friends);//a,b,c
console.log(b.friends);//a,b
console.log(a.friends === b.friends); //false
console.log(a.sayName === b.sayName); //true

动态原型模式

在构造函数中,if检查初始化后应存在的任何属性或方法.从而对构造函数和原型方法进行封装.

示例:

function Person(name,age,job){
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ["a","b"];
  //注意不要使用对象字面量重写原型
  if(typeof this.sayName != "function"){
    Person.prototype.sayName = function(){
      alert(this.name);
    };
  }
}

var a = new Person("jack",18,"programmer");
var b = new Person("james",20,"designer");
a.friends.push("c");
console.log(a.friends);//a,b,c
console.log(b.friends);//a,b
console.log(a.friends === b.friends);//false
console.log(a.sayName === b.sayName);//true
console.log(a instanceof Person);//true
console.log(b instanceof Person);//true

寄生构造函数模式(不推荐

相当于工厂模式,通常用于在特殊情况下为对象创建构造函数,如我们要创建一个具有额外方法的特殊数组,又不能直接修改Array构造函数,就可以使用该模式.
注!返回对象和构造函数外部创建对象没有不同,所以无法确定对象类型.不推荐使用

示例:

function SpecialArray(){
    var values = new Array();
    //添加值
    values.push.apply(values,arguments);
    //添加方法
    values.toPipedString = function(){
        return this.join("|");
    };
    return values;
}

稳妥构造函数模式(不推荐

稳妥对象:没有公共属性,方法都不引用this的对象
和寄生构造函数模式的相似点:

  1. 创建对象实例不引用this

  2. 不使用new操作符调用构造函数

  3. instanceof无效

注意,稳妥对象中,除了定义的方法之外没有其他方法访问某值.
注!和寄生构造函数模式一样,不推荐使用

示例:

function Person(name,age,job){
    var o  = new Object();
    o.sayName = function(){
        alert(name);
    };
    return 0;
}

var friend =Person("Jack",18,"Software Enginner");
friend.sayName();

继承

在ECMAScript中支持的是实现继承,并且其实现继承主要依靠原型链实现,所以明白原型链就很重要了.

  1. 原型链

  2. 借用构造函数

  3. 组合继承

  4. 原型式继承

  5. 寄生式继承

  6. 寄生组合式继承

原型链

基本思想:利用原型链让一个引用类型继承另一个引用类型的属性和方法.
注!和我们之前提到的一样,所有函数的默认原型都是Object的实例.内部指针->Object.prototype

原型与实例的关系

instanceof操作符,可以测试实例与原型链中的构造函数.
isPrototypeOf()方法 ,与instanceof操作符返回效果相同.

谨慎定义方法

子类重写超类/父类中某个方法,或者添加父类/超类不存在的某个方法时,要放在替换原型语句后.
注!不要使用对象字面量创建原型方法,这会重写原型链

原型链问题

  1. 引用类型问题

  2. 创建子类型实例时不能(或者说没办法在不影响所有对象实例的情况下)向超类型的构造函数传递参数.

根据上述问题,实践中很少单独使用原型链.

示例:

function SuperType(){//父类/超类
  this.property = true;
}
SuperType.prototype.getSuperValue = function(){
  return this.property;
};

function SubType(){//子类
  this.subproperty = false;
}
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function(){
  return this.subproperty;
}
// 谨慎定义方法
//SubType.prototype.getSuperValue=function(){
//  return false;
//}该方法会屏蔽原来的方法,即通过SuperType的实例调用getSuperValue时依然调用原来的方法,而通过SubType的实例调用时,会执行这个重新定义的方法.必须在SubType.prototype = new SuperType();之后,再定义getSubValue和该方法.

var a = new SubType();
console.log(a.getSubValue());//false
console.log(a.getSuperValue());//true
//原型与实例的关系
console.log(a instanceof Object);//true
console.log(a instanceof SuperType);//true
console.log(a instanceof SubType);//true

借用构造函数

伪造对象/经典继承.
目的:解决引用类型问题->借用构造函数(constructor stealing)
基本思想:子类型构造函数内部调用超类/父类构造函数
缺点:无法避免构造函数模式存在的问题(函数无法复用)
所以该方式很少单独使用.

示例:

function SuperType(name){
  this.name = name;
  this.arr = ["a","b","c"];
}
function SubType(){
  SuperType.call(this,"jack");//传递参数
  this.age = 18;//实例属性
}
var a = new SubType();
a.arr.push("d");
var b = new SubType();
console.log(a.arr);//a,b,c,d
console.log(b.arr);//a,b,c

组合继承

combination inheritance
也称伪经典继承,将原型链和借用构造函数技术结合一起的继承模式.
基本思想:使用原型链实现对原型属性和方法的继承,借用构造函数实现对实例属性的继承.constructor重指向
相当于:属性继承(借用构造函数),函数外定义方法,constructor重新指向

组合继承避免了原型链和借用构造函数的缺陷,融合了优点,成为了JS中最常用的继承模式,而且instanceofisPrototypeOf()都能够识别

示例:

function SuperType(name){
  this.name = name;
  this.arr = ["a","b"];
}
SuperType.prototype.sayName =function(){
  console.log(this.name);
};

function SubType(name,age){
  SuperType.call(this,name);
  this.age = age;
}

//inherit
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
  console.log(this.age);
};

var a = new SubType("Jack",18);
a.arr.push("c");
console.log(a.arr);//a,b,c
a.sayName();//Jack
a.sayAge();//18

var b = new SubType("James",20);
console.log(b.arr);//a,b
b.sayName();//James
b.sayAge();//20

原型式继承

Prototypal inheritance
将传入的对象作为函数内定义的构造函数的原型(要求必须有一个对象可以作为另一个对象的基础),在ECMAScript5中新增Object.create()方法规范了原型式继承,它接收两个参数,一个用作新对象原型的对象和(可选)一个为新对象定义额外属性的对象.
单个参数情况下Object.create()Object()行为相同
兼容性:IE9+,Firefox4+,Safari5+,Opera12+,Chrome
缺点:和原型模式一样,引用类型共享.

示例:

function object(o){
  function F(){};
  F.prototype = o;
  return new F();
}

var person = {
  name: "Jack",
  arr: ["a","b"]
};

var a = object(person);
var b = object(person);
a.name = "James";
a.arr.push("c");
b.name = "Ansem";
b.arr.push("d");
console.log(person.arr);//a,b,c,d
console.log(a.arr);//a,b,c,d
console.log(b.arr);//a,b,c,d

//Object.create
var person2 = {
  name: "Jack",
  arr: ["a","b"]
};

var c = Object.create(person2,{
  name:{
    value: "James"
  }
});
var d = Object.create(person2,{
  name:{
    value: "Ansem"
  }
});
c.arr.push("c");
d.arr.push("d");
console.log(c.name);//James
console.log(d.name);//Ansem
console.log(person.arr);//a,b,c,d
console.log(c.arr);//a,b,c,d
console.log(d.arr);//a,b,c,d

寄生式继承

parasitic inherit
思路与寄生构造函数和工厂模式类似,创建新对象,增强对象,返回对象.
缺点:函数复用不了,对于引用类型为共享.

示例:

function createAnother(original){
  var clone = object(original);
  clone.sayHi = function(){
    console.log("HI");
  };
  return clone;
}

寄生组合式继承(重点)

组合继承的问题:无论什么情况都会两次调用超类型构造函数
第一次:SubType.prototype = new SuperType()
第二次:new SuperType()内->SuperType.call(this,name);
这造成的结果是,第一次时:SuperType的实例(SubType的原型)初始化属性.第二次时:新对象上又新创建了相同的属性,于是这两个属性就屏蔽了原型中两个同名属性.

解决方法就是寄生组合式继承.通过借用构造函数来继承属性,通过原型链的混成形式来继承方式.
基本思路:不必为了指定子类型的原型而调用超类/父类的构造函数,我们需要的知识超类/父类原型的一个副本.在这点上使用寄生式继承来继承超类/父类的原型,再将结果指定给子类的原型.

高效率体现在避免了创建多余不必要的属性,原型链还能保持不变.instanceofisPrototypeOf()都能正常使用.

可以说寄生组合式继承是引用类型最理想的继承范式,这也被YUI库所采用.

示例:

//基本模式
function inheritPrototype(subType,superType){
  var prototype = Object(superType.prototype);
  prototype.constructor = subType;
  subType.prototype = prototype;
}
function SuperType(name){
  this.name = name;
  this.arr = ["a","b"];
}
SuperType.prototype.sayName =function(){
  console.log(this.name);
};
function SubType(name,age){
  SuperType.call(this,name);
  this.age = age;
};

inheritPrototype(SubType,SuperType);//避免了多次执行,提高了效率

SubType.prototype.sayAge = function(){
  console.log(this.age);
};

var c = new SubType("Jack",18);
var d = new SubType("Ansem",25);

c.arr.push("c");
d.arr.push("d");
console.log(c.name);//Jack
console.log(d.name);//Ansem
console.log(c.arr);//a,b,c
console.log(d.arr);//a,b,d