JavaScript面向对象详解

发布时间:2023-11-02 16:30

声明:本人的所有博客皆为个人笔记,作为个人知识索引使用,因此在叙述上存在逻辑不通顺、跨度大等问题,希望理解。分享出来仅供大家学习翻阅,若有错误希望指出,感谢!

面向对象

  • JavaScript中没有类的概念
  • 我们可以把对象看作散列表,无非就是一组名值对,值可以是数据或函数
  • 创建对象最简单的方法就是创建一个Object对象,再为他添加属性和方法

属性类型

JavaScript中有两种属性,数据属性和访问器属性

数据属性

  • 数据属性有四个描述其行为的特性
属性特性 说明 默认值
[[Configurable]] 能否通过delete删除属性从而重新定义属性,能否修改属性的特性,能否把属性修改为访问器属性 true
[[Enumerable]] 能否通过for-in循环返回属性 true
[[Writable]] 能否修改属性的值 true
[[Value]] 包含这个属性的数据值,读属性时,从这个位置读;写属性时,从这个位置写 undefined
  • 要修改属性默认的特性,必须使用Object.defineProperty()方法
Object.defineProperty(对象名,"属性名",{	//若对象中没有该属性,则会创建该属性
	//以下四个不用全写,注意开头为小写
	configurable : truefalse,
	enumerable : truefalse,
	writable : truefalse,
	value :});

注意:

  • 一旦把属性定义为不可配置(configurable : false),就不能再改回去了,此时调用Object.defineProperty()方法修改writable以外的特性都会导致错误
  • Object.defineProperty()方法创建一个新属性时,如果不指定,则configurable、enumerable、writable的默认值都是false

访问器属性

访问器属性不包含数据值,它们包含一对getter和setter函数(这两个函数不是必须的),在读取访问器属性时,会调用getter函数,在写入访问器属性时,会调用setter函数

访问器特性 描述 默认值
[[Configurable]] 能否通过delete删除属性从而重新定义属性,能否修改属性的特性,能否把属性修改为数据属性 true
[[Enumerable]] 能否通过for-in循环返回属性 true
[[Get]] 读取属性时调用的函数 undefined
[[Set]] 写入属性时调用的函数 undefined
  • 要修改属性默认的特性,必须使用Object.defineProperty()方法
var object_name = {
    _attribute : 属性值	//以开头加 _ 表示只能通过对象方法访问的属性,语法习惯,不强求
};

Object.defineProperty(object_name,"attribute",{		//此处attribute前没有_ ,attribute是
	//以下四个不用全写,注意开头为小写						_attribute的访问器属性
	configurable : truefalse,
	enumerable : truefalse,
	get : function(){
        ……
        return this._attribute;
    },
    set : function(newValue){
        ……
        this._attribute = newValue;
    }
});

object_name.attribute = 新属性值;	//此处后台调用了set方法
var a = object_name.attribute;	   //此处后台调用了get方法
  • 不一定非要同时指定getter和setter
    • 只指定getter意味着属性不能写,尝试写入会被忽略,严格模式下会报错
    • 只指定getter意味着属性不能读,尝试读取会返回undefined,严格模式下会报错

一次性定义多个属性

Object.defineProperties()方法

Object.defineProperties(对象名,{
    属性1:{
        特性1:特性值1,
        特性2:特性值2,
        …………
    },
    属性1:{
        特性1:特性值1,
        特性2:特性值2,
        …………
    },
    …………
});

读取属性的特性

Object.getOwnPropertyDescriptor(对象名,"属性名");

返回值是一个对象

如果是访问器属性,这个对象的属性有configurable、enumerable、get、set

如果是数据属性,这个对象的属性有configurable、enumerable、writable、value

创建对象

工厂模式

用函数来封装创建对象的细节

function 工厂函数名 (参数列){
    var o = new Object();
    o.属性1 = 参数1;
    o.属性2 = 参数2;
    …………
    o.方法1 = function (参数列){
        函数体;	//此处使用o的属性应当写this.属性名
    }
}

构造函数模式

function 构造函数名(参数列){		//JavaScript没有类,构造函数名相当于类名
    this.属性1 = 参数1;
    this.属性2 = 参数2;
    …………
    this.方法1 = function (参数列){
        函数体;
    }
}

var 变量名 = new 类名(参数列);

要使用构造函数创建实例,必须使用new操作符,具体过程如下:

  1. 创建一个新对象
  2. 将构造函数的作用域赋给新对象(this指向了这个新对象)
  3. 执行构造函数中的代码
  4. 返回新对象

构造函数特点

  • 用构造函数创建的对象既是Object的实例,也是构造方法名字的实例

    • 这意味着可以将他的实例标识为一种特定的类型
  • 构造函数和普通函数唯一的区别就是调用方式不同,构造函数必须用new操作符调用

    • 任何函数,只要用new调用,就都会被当作构造函数
    • 如果不使用new操作符,构造函数仍能作为普通函数使用,此时构造函数中的this为调用该函数的作用域,相当于为此作用域对象构建属性和方法

构造函数的问题

构造函数的问题在于:每个方法要在每个实例上重建一遍

  • 由于函数也是一个对象,故使用构造函数创建对象方法时,实际上是创建了一个新的函数对象,这将导致每一个实例都拥有一个独立的函数对象,且这些函数对象的功能完全相同,这是极为浪费资源的做法

解决办法:

  • 1.把函数定义转移到构造函数外部

    function 构造函数名(参数列){
        …………
        this.方法1 = 函数名1;
    }
    
    function 函数名1(参数列){
        函数体:
    }
    
  • 2.原型模式

原型模式

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

不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中

function 构造函数名(){}

构造函数名.prototype.属性名1 = 属性值1;
构造函数名.prototype.属性名1 = 属性值1;
…………
构造函数名.prototype.方法名1 = function(参数列){
	函数体:
}

原型模式与构造函数模式最大的区别是新对象的属性和方法是所有实例共享的,不能通过实例修改,只能修改原型对象,为共享的属性赋值将会创建一个新的实例属性覆盖原型对象中的属性

简化的原型语法(对象字面量形式)

function 构造函数名(){}

构造函数名.prototype = {		//创建新对象,重写prototype属性,相当于prototype = new Object()
	属性名1 : 属性值1,		//多个键值对用逗号隔开,最后一个不用加逗号
	属性名2 : 属性值2,
	…………
	方法名1 : function(参数列){
		函数体:
	},
	…………	
}
//此处以后才可以创建实例

注意:

  • 对象字面量形式本质为创建新对象,该构造函数的prototype属性被重写

    因此必须在构造函数名.prototype = {……} 语句后才能创建实例

  • 以这种方式创建实例,原型对象的constructor属性不再指向构造函数,尽管instanceof操作符仍能返回正确的结果,但通过constructor属性以及无法确定对象类型了

  • 如果constructor属性很重要,可以在设置原型对象时手动设置constructor属性

    构造函数名.prototype = {
    	constructor : 构造函数名,
    	…………
    }
    
    • 以这种方式重设constructor属性会导致它的[[Enumerable]]特性被设置为true(默认为false,不可枚举)
      • 可以用Object.defineProperty() 方法重新设置[[Enumerable]]特性

理解原型对象

  1. 只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象
  2. 在默认情况下,所有原型对象都会自动获得一个constructor属性,这个属性是指向prototype属性所在函数的指针
  3. 创建自定义构造函数后,其原型对象默认只会取得constructor属性,当调用构造函数创建一个新实例后,该实例的内部包含一个指针,指向构造函数的原型对象

设有构造函数 function A ( ){……} var a = new A(); 则:

  • A.prototype.constructor 指向 A
  • a有一个内部值 [[prototype]] 指向构造函数的原型对象,[[prototype]] 可以用 a.__proto__来访问
    • Object.getPrototypeOf()方法可以返回[[prototype]]的值
      • Object.getPrototypeOf(a)指向A.prototype
原型对象属性与实例属性的关系
  • 每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性,搜索首先从对象实例本身开始,若找到则返回该属性的值,如果没找到,则继续搜索指针指向的原型对象

  • 虽然可以通过对象实例访问保存在原型中的值,却不能通过对象实例重写原型中的值,如果我们在实例中添加了一个属性,该属性与原型中一个属性同名,则该属性会屏蔽掉原型中的那个属性

  • 使用delete删除掉实例属性后,就能重新访问原型中的属性了

  • 使用hasOwnProperty()方法可以检测一个属性存在于实例还是存在于原型中,当给定属性存在于对象实例时,返回true,该函数是从Object中继承来的

    实例.hasOwnProperty("属性名");
    

原型与in操作符,属性的遍历

有两种方法使用in操作符:单独使用、在for-in循环中使用

  • 直接使用

    "属性名" in 对象名	//通过对象能够直接访问给定属性时返回true,无论该属性在实例中还是在原型中
    

    使用hasOwnProperty()和in操作符就可以确定该属性到底存在于对象中,还是存在于原型中

    //若属性存在于原型中,返回true;若属性存在于实例中,返回false
    function hasPrototypeProperty(object,attribute_name){
        return !object.hasOwnProperty(attribute_name) && (attribute_name in object);
    }
    
  • for-in

    • 使用for-in循环时,返回的是所有能够通过对象访问的、可枚举的属性,无论该属性在实例中还是在原型中,屏蔽了原型中的不可枚举属性的实例属性也会在for-in循环中返回

属性的遍历

  • for-in
  • Object.keys(对象) 方法返回一个包含传入对象所有可枚举属性的字符串数组
  • Object.getOwnPropertyName(对象) 方法返回一个包含传入对象所有属性的字符串数组(无论是否可枚举)

原型的动态性

我们对原型对象所做的任何修改都能够立即从实例上反映出来,即使先创建了实例再修改也是如此

  • 因为实例与原型之间的连接是一个指针
  • 实例中的指针指向原型,而不指向构造函数

绝对不能重写原型对象

  • 因为重写原型对象后,实例的指针仍然指向旧的原型对象,这会切断现有原型与之前存在的对象实例之间的联系
  • 故如果使用简化的原型语法(对象字面量形式),则构造函数的prototype属性被重写,因此如果要使用对象字面量形式,就只能在构造函数的prototype属性重写完成后,才能创建实例

原型对象的问题

他省略了为构造函数传递参数之一环节,结果所有实例在默认情况下都能取得相同的属性值

若原型对象包含引用类型值,则会导致多个实例操作同一个引用对象,这是不合理的,每个实例应当拥有自己独立的属性值

(最常用)组合使用构造函数模式与原型模式

创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式

构造函数模式用于定义实例属性,原型模式用于定义方法和共享的属性

function object_name(参数列){
    this.属性名1 = 属性值1;
    this.属性名2 = 属性值2;
    …………
}
object_name.prototype = {
    constructor : object_name,
    共享属性名 : 共享属性值,
    …………
    方法名 : function(参数列){
        函数体;
    },
    …………
}

var 实例名 = new object_name(参数列);

动态原型模式

在构造函数中初始化原型,既书写简便,又保持了组合使用构造函数模式与原型模式的优点

function object_name(参数列){
    this.属性名1 = 属性值1;
    this.属性名2 = 属性值2;
    …………
    if(typeof this.方法1 != "方法1"){	//仅在第一次调用构造函数时修改原型对象
    	object_name.prototype.共享属性名 = 共享属性值;  //不能使用字面量写法,字面量写法相当于重写													prototype,将会切断实例与新原型之间的连接
        …………
        object_name.prototype.方法 = function(参数列){
            函数体;
        }
    }
}
  • 由于所有方法和共享属性是一起定义的,因此if语句中只需要确定一个方法的存在即可(一个方法存在则所有方法存在)

寄生构造函数模式

function 构造函数名 (参数列){
    var o = new Object();
    o.属性1 = 参数1;
    o.属性2 = 参数2;
    …………
    o.方法1 = function (参数列){
        函数体;	//此处使用o的属性应当写this.属性名
    }
    return o;
}
var 实例名 = new 构造函数名(参数列);
  • 除了使用new操作符并把包装函数叫做构造函数之外,这个模式其实和工厂函数一模一样

  • 通过在构造函数末尾添加 return 语句,可以重写调用构造函数时返回的值

  • 用于想要修改某类型构造函数,却又不能修改时使用

  • 返回的对象与构造函数或构造函数的原型之间没有关系

  • instanceof操作符无法确定其实例的类型

  • 在能够使用其他模式的情况下,不要用这种模式

稳妥构造函数模式

  • 稳妥对象:安全性高
    • 没有公共属性
    • 其方法也不引用this对象
  • 稳妥构造函数的特点:
    • 创建对象的实例方法不引用this
    • 不使用new操作符调用构造函数
function object_name(参数1,参数2,……){
    var o = new Object();
    //此处定义私有变量和方法
    o.getparam1 = function(){
        return 参数1;			//此处的参数1并不是一个属性,它只能通过该方法访问
    }
    return o;
}
var 实例名 = object_name("2233",……);
实例名.getparam1();		//返回“2233”

继承

由于函数没有签名,在JavaScript中无法实现接口继承,只支持实现继承,其实现继承主要是依靠原型链实现的

原型链

利用原型让一个引用类型继承另一个引用类型的属性和方法

原理:让原型对象等于另一个类型的实例

function SuperType(){	//父类构造函数
    //定义父类的属性
    this.属性名 = 属性值;
    …………
}

SuperType.prototype.方法 = function(……){……}	//定义父类的方法
…………

function SubType(){		//子类构造函数
    //定义子类的属性
    this.属性名 = 属性值;
    …………
}

SubType.prototype = new SuperType();	//将子类的原型指定为父类的一个实例,该实例拥有父类的全部属性和方										法,从而实现继承,必须在该行之后才能定义子类方法
SubType.prototype.方法 = function(……){……}	//定义子类的方法
  • 子类实例 —> 子类原型对象 = 父类实例 —> 父类原型对象
  • 子类实例的constructor指向父类
  • 通过原型链实现继承,会使属性方法的搜索过程得以沿着原型链继续向上
  • 注意:所有函数默认都是Object的实例,因此默认原型都有一个内部指针指向Object.prototype
确定原型和实例的关系
  • 使用instanceof操作符测试实例与原型链中出现过的构造函数,结果就会返回true
  • 构造函数名.prototype.isProtoTypeOf(实例):若该实例的原型链中有该构造函数,则返回true
谨慎的定义方法
  • 无论是要覆盖父类的方法,还是要添加父类没有的方法,给原型添加方法的代码一定要放在替换原型的语句后
  • 通过原型链实现继承时,不能使用对象字面量创建原型方法,这样会重写原型链
原型链的问题
  1. 通过原型实现继承,原型会变成父类的实例,则父类的实例属性变成了子类的原型属性,而实例属性不应该作为原型属性
  2. 在创建子类实例时,不能向父类的构造函数传递参数

因此实际工作中很少用到纯原型链

借用构造函数

  • 在子类型构造函数内部调用超类型构造函数
  • 函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在新创建的对象上执行构造函数
function SubType(){
	SuperType.call(this,参数列);		//使用此种方式创建的子类对象本质上是父类对象实例
}
var 实例名 = new SubType();
  • 借用构造函数的最大优势:可以在子类构造函数中向父类构造函数传递参数
  • 为了确保SuperType构造函数不会重写子类属性,必须在调用父类构造函数后再添加子类的属性
  • 借用构造函数的问题:
    • 与使用构造函数模式的缺点相同,无法实现函数服用

组合继承

将原型链和借用构造函数技术组合到一起

  • 使用原型链实现对原型属性和方法的继承
  • 使用借用构造函数方法实现对实例属性的继承
//定义父类
function SuperType(参数列){
	this.父类属性 = 属性值;
    …………
}
SuperType.prototype.父类方法 = function(……){……}

//定义子类
function SubType(参数列){
    SuperType.call(this,参数列);
	this.子类属性 = 属性值;
    …………
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
//定义子类方法
SubType.prototype.子类方法 = function(……){……}

var 实例名 = new SubType(属性列);
  • 本方法是JavaScript中最常用的继承模式
  • instanceof和isPrototypeOf()能够识别基于组合继承创建的对象
  • 组合式继承的最大缺点:无论在什么情况下,都会调用两次父类构造函数,一次是在创建子类原型的时候,另一次是在子类型构造函数内部

原型式继承

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

function object(o){		//o为已有对象,作为新对象的原型
	function F(){}
	F.prototype = o;
	return new F();
}

Object.create()方法亦可实现上述功能

var 实例名 = Object.create(作为新对象原型的对象,{
                        属性名1 : {			//此处的写法与设置属性特性相同
                        	value : 属性值,
                        	…………
                        }
						属性名2 : {设置特性}
						…………
					})

寄生式继承

创建一个仅用于封装继承过程的函数

function create(original){			//original为对象原型
	var clone = object(original);	//此处object()为原型式继承的方法
    clone.属性名 = 属性值;			//以某种方式增强这个对象
    clone.方法名 = function(……){……}  //此处定义的函数无法实现复用
    return clone;					//返回这个对象
}

任何能返回新对象的函数都适用于此模式

使用寄生式继承为对象添加函数,会因为无法实现函数复用而降低效率

寄生组合式继承

通过借用构造函数来继承属性,通过原型链的混成形式来继承方法

function inheritPrototype(subType,superType){
	var prototype = object(superType.prototype);	//此处object()为原型式继承的方法
    prototype.constructor = subType;
    subType.prototype = prototype;
}

//定义父类
function SuperType(参数列){
	this.父类属性 = 属性值;
    …………
}
SuperType.prototype.父类方法 = function(……){……}

//定义子类
function SubType(参数列){
    SuperType.call(this,参数列);
	this.子类属性 = 属性值;
    …………
}
//子类继承父类
inheritPrototype(subType,superType);
//定义子类方法
SubType.prototype.子类方法 = function(……){……}

var 实例名 = new SubType(属性列);
  • 寄生组合式继承是引用类型最理想的继承范式

ItVuer - 免责声明 - 关于我们 - 联系我们

本网站信息来源于互联网,如有侵权请联系:561261067@qq.com

桂ICP备16001015号