JavaScript对象、原型链、继承、闭包

什么是面向对象编程

说到面向对象,每个人的理解可能不同,以下是个人对面向对象编程的理解:

对于面向对象编程这几个字每一个前端都应该非常熟悉,但是到底应该如何去理解他呢?
就编程方式而言,javascrip可以分成两种发方式:面向过程和面向对象,所谓的面向过程就是比较常用的函数式编程,通过大量的单体函数调用来组织代码,这种方式使得代码结构紊乱,不方便代码阅读,由于定义了大量的函数而耗费内存;而面向对象则是另一种开发方式,面向对象的标志就是类,但是js中却没有类的概念,那么js中的面向对象又改如何理解呢?我们可以将构造函数(构造器)理解为类,通过定义构造函数,实例化对象,基于prototype实现对象继承,改变传餐数量控制方法功能的方式,同样体现面向对象编程封装、继承、多态的特点。

对象

无序属性的集合,其属性可以包含基本值、对象或者函数

js中有万物皆对象的说法,那么当我们看到 var a = 'asdfqwert',那么这种说法还正确吗?

var a = 'asdfqwert'
typeof a      //'string'

类型判断是返回string,那么说明a是一个简单类型,但是实际我们却可以调用a.split a.toString 等方法,这个时候实际会在内部把a 转为String对象的实例,由于对象、函数本身就是Object,那么说万物皆对象完全没有问题。

那么创建对象的方式又有那些呢?

创建对象的方式

Object构造函数

var obj1 = new Object(); //或者var obj = new Object({name:'object',age;12});
obj.name='object'; obj.age = 12

对象字面量

var obj2 = {
    name:'Join',
    age:23,
    getName:function(){ return this.age } }

通过Object构造函数和对象字面量的方式创建一个实例对象,比较适合创建单个或者少量实例对象,通过这种方式创建大量的相似对象时,代码重复,冗余。

工厂模式

function Fn(name,age){
    var obj = ne Object(); obj.name = name; obj.age = age; obj.getName = function(){ return this.name } return obj } var person1 = Fn('11',10) var person2 = Fn('11',1yu0)

工厂模式实际是一个包含参数的函数封装,调用后返回拥有参数和方法的对象,多次调用解决重复代码问题,但是存在对象识别的问题,同时也拥有构造函数模式相同的缺点

构造函数模式

function Fn(name,age){
    this.name = name; this.age = age; this.getName = function(){ return this.name; } } var person1 = new Fn('啦啦',18) var person2 = new Fn('菲菲',17)

通过自定义构造函数可以创建特定类型,具有类似属性和功能的的实例对象。例如,通过Fn创建出大量不同姓名,不同年龄,但是都有获取实例姓名属性的实例个体。只需要通过 new 关键字实例化就可以了,代码量相比于字面量的方式大大减少。

但是,其缺点就是在new的过程中多个实例相同(共用)的属性和方法在每个实例上保留一份,耗费内存。

提到new,这里说一下new的过程,发生了什么,实际上以下代码体现了new的过程

//构造函数A,new 一个A的实例a
function A (){
    this.name = 111 } var a = new A('梅梅') //new 的过程 var obj = new Object(); //创建一个新对象 obj.__proto__ = A.prototype; //让obj的__proto__指向A.prototype A.call(obj) //在obj的作用环境执行构造函数的代码,使this指向obj return obj //返回实例对象

原型模式

function Fn(){
   
}
Fn.prototype.name='啦啦' Fn.prototype.getName = function(){ return this.name; } var a = new Fn() var b = new Fn() a.getName() //啦啦 b.getName() //啦啦 a.getName == b.getName //true

函数创建会拥有prototype属性,指向一个存放了某些特定实例的共用属性和方法的对象。构造函数Fn的实例a和b都可以调用Fn原型对象上的方法getName,得到的name值是一样的。a.getName == b.getName,说明调用的是同一个getName方法,在构造函数的原型对象上,所以原型对象上的方法和属性是其所有实例共用的。
通过原型对象可以解决构造函数模式提到每个实例保留一份的内存耗费问题

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

function Fn(name,age){
   this.name = name; this.age = age; } Fn.prototype.getName = function(){ return this.name; } var a = new Fn('啦啦',23) var b = new Fn('呼呼',15) a.getName() //啦啦 b.getName() //呼呼

通过两者组合的方式可以实现每个实例对象拥有一份自己的实例属性,同时还能够访问构造函数原型对象上的共享的原型属性和原型方法。同时拥有构造函数模式和原型模式创建对象的优点。

动态原型模式和寄生构造函数模式这里不多说,有兴趣的可以自己看看

稳妥构造函数模式

function Fn(name,age){
    var obj = new Object(); var name = name; var age = age obj.getName = function(){ return name } obj.setName = function(n){ name = n } return obj; } var fn = Fn('家家',23) fn.getName() //'家家' fn.setName('lala') fn.getName() //'lala'

所谓的稳妥构造函数实际上本意是起到保护变量的作用,只能通过对象提供的方法操作构造函数传入的原始变量值,防止通过其他途径改变变量的值,其实质是闭包。

作用域链

作用域

作用域分为全局作用域和函数作用域,简单粗暴的理解就是,变量的可访问区域。

var a = 111;
function b(){ var c = 222 } b() console.log(a) //111 console.log(c) //报错 c is not undefined

同样是定义变量,但是定义在函数内部的变量,在函数外部无法访问,说明c的可访问区域只在函数内部。

执行环境(execution context)
执行环境始终是this关键字的值,它是拥有当前所执行代码的对象的引用,函数的每次调用都会创建一个新的执行环境。

当执行流进入一个函数时,函数的执行环境就会被推入环境栈顶端,执行结束后环境栈将其弹出并销毁,把控制权还给之前的执行环境。如下图所示:

图片描述

变量对象(函数执行时转为活动对象)
每个执行环境都有一个与之关联的变量对象,环境中的变量和函数都保存在对象中。当代码执行结束并且环境中变量和函数不被其他对象引用,那么变量对象随着执行环境销毁。

作用域链
当代码在环境中执行时,会创建一个变量对象的作用域链,其作用就是保证对执行环境有权访问的变量和函数做有序访问。作用域链的最前端永远都是当前执行的代码所在执行环境的变量对象。作用域链中下一个变量对象为外层环境的变量对象,依次类推至最后全局对象。

function fn1(){
    var a = 1; return function fn2(){ return a; } } var b = fn1() var c = b() //1

图片描述

上图所表现的就是fn1的执行环境,当外层函数调用时,内层函数的作用域链就已经创建了。

执行环境、变量对象、作用域链之间的关系

上图体现执行环境、作用域链和变量对象(活动对象)之间的关系。调用fn1返回函数fn2 ,fn2内部访问了fn1中定义的a,当fn2调用时返回a=1,但是fn2中没有定义1,所以会顺着作用域链向上找,直至找到a,没有则报错。

因为这里应用了闭包,当fn1执行结束,fn1的执行环境会销毁,但是由于a被fn2访问,所以fn1作用域链会断开,但是变量对象保留,供fn2访问。

原型链

原型 prototype

每个函数在创建的时候都会有一个prototype属性,该属性指向一个对象,用于存放可以被有所属函数创建出来的所有实例共用的属性和方法。

function A(){}
A.prototype.name="共用属性" A.prototype.getName=function(){ return this.name } var a = new A(); var b = new A(); a.getName() //"共用属性" b.getName() // "共用属性" console.log(a.name == b.name,a.getName == b.getName) //true true

以上代码体现的就是原型对象的特点,所有A构造函数的实例共用其原型对象上的方法和属性,同时由原型对象组成的原型链也是实现继承的基础。

原型的链理解

原型链的形成离不开构造函数、原型对象和实例,这里简单介绍下原型链的形成原理:

每个构造函数在创建的时候都会有一个prototype(原型)属性,该属性指向的对象默认包含constructor(构造函数)属性,constructor同样是一个指针,指向prototype所在的函数,当然我们可以想prototype添加其他属性和方法。当我们创建一个构造函数的实例时,该实例对象会包含有一个内部属性[[prototype]],通常叫__proto__,__proto__指向创建实例的构造函数的原型对象。

当我们让一个函数的prototype指向一个实例,那么会发生什么?

function A(){
    this.name = 'lala'; } A.prototype.sayHi = function(){ console.log('hi') } function B(){} B.prototype = new A(); var instance = new B() console.log(instance)

图片描述

上图为B的实例instance的输出结果。
instance.__proto__指向 B.prototype,B.prototype == new A() A的实例,(new A()).__proto__ = A.prototype,即:
instance.__proto__ == B.prototype.__proto__ == A.prototype,所以B的实例可以通过这种链的形式访问A的原型方法和原型属性,由于A实例化的时候复制了A的实例属性,所以instance可以访问复制到B.prototype上得name属性。

javascript的基于原型链的继承原理也是这样的,下面对继承的原理就不再重复说明。
通过下图可以更加直观的反应原型链和继承的原理:

图片描述

继承

类的三部分

构造函数内的:供实例化对象复制用的
构造函数外的:通过点语法添加,供类使用,实力化对象无法访问
类的原型中的:供所有实例化对象所共用,实例化对象可以通过其原型链间接的访问
(__proto__:总是指向构造器所用的原型,constructor:构造函数)

类式继承

  • 实现方式:类式继承需要将第一个类的实例赋值给第二个类的原型
  • 继承原理:父类的实例赋值给子类的原型,那么这个子类的原型同样可以访问父类原型上的属性和方法与从父类构造函数中复制的属性和方法
  • 本质:重写子类的原型对象(导致子类的圆形对象中的constructor的指向发生改变,变为父类构造函数)
  • 特点:实例即是子类的实例也是父类的实例;父类新增的原型方法和原型属性实例均可以访问,并且是所有实例共享
  • 缺点:如果父类中的共有属性是引用类型,那么就会被子类的所有实例共用,其中一个子类的实例改变了从父类继承来的引用类型则会直接影响其他子类;创建子类实例时,无法向父类构造函数传参;无法实现多继承
function SuperClass(){ //父类 this.superName = '类式继承-父类'; this.superArr = ['a','b']; } SuperClass.prototype.getSuperName = function(){ //父类原型方法 return this.superName; } function SubClass(){ //子类 this.subName = '类式继承-子类'; this.subArr = [1,2]; } var superInstance = new SuperClass(); //父类实例 SubClass.prototype = superInstance; //继承父类 SubClass.prototype.getSubName = function(){ //子类原型方法 return this.subName; } var instance1 = new SubClass(); //子类的实例 var instance2 = new SubClass();//子类的实例 console.log(instance1.superArr, instance1.getSuperName(), instance1.subArr, instance1.getSubName()); //'a,b' '类式继承-父类' [1,2] '类式继承-子类' instance1.subArr.push(3); //更改子类的实例属性引用值 instance1.superArr.push('c');//更改父类的实例属性引用值 console.log(instance2.superArr,instance2.subArr) // 'a,b,c' [1,2] 由于是引用类型所以其他实例上的属性也被更改

构造函数继承

  • 实现方式:在子类内对父类执行SuperClass.call(this,name...)语句同时传入this和指定参数
  • 实现原理:通过call函数改变执行环境(this的指向),使得子类复制得到父类构造函数内的属性并进行自定赋值,同时子类也可以有自己的属性(没有用到原型)
  • 本质:父类构造函数增强子类实例
  • 特点:改善类式继承中的父类的引用属性被所有子类实例共用的问题;可以在子类内call多个父类构造函数来实现多继承;子类实例化时可向父类传参
  • 缺点:只能继承父类的实例属性和实例方法,无法继承父类的原型属性和原型方法;通过子类new出来的实例只是子类的实例,不是父类的实例;影响性能
function SuperClassA(name){
    this.superName = name; this.books = ['aa','bb']; } SuperClassA.prototype.getSuperName = function(){ return this.superName; } function SubClassA(name,age){ SuperClassA.call(this,name);//通过call继承,new一次就复制一次 this.age = age; } var instance1 = new SubClassA('莉莉',21); var instance2 = new SubClassA('佳佳',23); instance1.books.push('cc'); console.log(instance1.superName,instance1.age,instance1.books,instance2.superName,instance2.age,instance2.books,instance1.getSuperName); //莉莉 , 21 , ["aa", "bb", "cc"] , 佳佳 , 23 , ["aa", "bb"] , undefined console.log(SubClassA.prototype) //只包含constructor属性的对象 

组合继承

  • 实现方式:在子类构造函数中执行弗雷构造函数,在子类原型上实例化父类
  • 实现原理:类式继承+构造函数继承,通过call函数改变执行环境(this的指向),使得子类复制得到父类构造函数内的属性并进行自定赋值,同时子类也可以有自己的属性,再通过类式继承的原型来继承父类的原型属性和原型方法
  • 本质:父类构造函数增强子类实例+重写子类原型对象
  • 特点:实例属性,实例方法,原型属性,原型方法均可继承;解决引用属性共享的问题;可向父类传参;new出来的实例即是父类实例也是子类实例
  • 缺点:耗内存
function SuperClassB(name){
    this.superName = name; this.books = ['aa','bb']; } SuperClassB.prototype.getSuperName = function(){ return this.superName; } function SubClassB(name,age){ SuperClassB.call(this,name);//通过call继承,new一次就复制一次 this.age = age; } var instanceSuperClassB = new SuperClassB(); SubClassB.prototype = instanceSuperClassB; var instance1 = new SubClassB('莉莉',21); var instance2 = new SubClassB('佳佳',23); instance1.books.push('cc'); console.log(instance1.superName,instance1.age,instance1.books,instance2.superName,instance2.age,instance2.books,instance1.getSuperName()); //莉莉 , 21 , ["aa", "bb", "cc"] , 佳佳 , 23 , ["aa", "bb"] , 莉莉

原型式继承(只强调子类原型等于父类原型方式的严重问题,不推荐)

  • 实现方式:直接原型相等的原型继承方式,子类原型等于父类原型
  • 实现原理:原型链
  • 本质:原型链
  • 特点:不推荐
  • 缺点:子类原型等于父类原型的继承方式会导致修改子类原型会直接反应在父类的原型上;引用类型共用
//父类
function SuperClassC(){ this.superName = '原型继承-父类'; } SuperClassC.prototype.superTit = '父类原型属性'; SuperClassC.prototype.getSuperName = function(){ return this.superName; } //-------直接用原型相等继承------- function SubClassC(){ this.subName = '原型继承-子类'; } SubClassC.prototype = SuperClassC.prototype; var instance0 = new SubClassC(); //修改子类原型 SubClassC.prototype.subTit1 = '直接原型-子类原型属性'; //对子类原型对象的修改同时反映到了父类原型 console.log(SuperClassC.prototype.subTit1); //'直接原型-子类原型属性',修改子类原型父类受到影响

子类原型直接等于父类原型的继承方式虽然能够实现原型对象的继承,但是却有严重问题。所以并不推荐这种写法,如果只想继承原型对象上得属性和方法,可以通过间接的方式,如下

function instanceSuper(obj){
    var Fn = function(){}; Fn.prototype = obj; return new Fn() } SubClassC.prototype = instanceSuper(SuperClassC.prototype); var insranceSub = new SubClassC(); SubClassC.prototype.subTit = '子类属性' console.log(SubClassC.prototype) console.log(SuperClassC.prototype)

图片描述

以上代码通过间接的方式同样实现了只继承父类的原型方法和属性,但是修改子类原型,打印子类原型和父类原型后显示父类原型并为被修改。这种方式跟类式继承很像,不同点只是赋值给子类原型的实例是通过一个函数封装返回的实例。

继承的方式还有寄生式继承、寄生组合式继承,是对以上继承方式的封装,感兴趣的可以自己找资料看下。

闭包

闭包是指有权限访问另一个函数作用域中的变量的函数。

上面在说作用域链的时候提到内部函数可以访问外部函数的变量,由于函数执行时会创建作用域链,当前函数的活动对象处于最前端,当访问变量时如果当前函数的活动对象内没有则会查找作用域链的下一个也就是外层函数的变量对象,依次向上直到找到或者查找完全局对象仍没有则报错,然而外部函数却无法访问内部函数的变量。然而闭包的特点就是访问其他函数内的变量。当函数执行结束后其执行环境会、变量对象都会销毁,但是当其内的变量被其他函数引用访问时,即使执行环境销毁,作用域链断开,但是变量对象依然存在,供引用了其内变量的函数去访问。

function fn(){
    var i = 1; return function(){ console.log(++i) } } var fn1 = fn(); fn1(); //2 fn1(); //3 fn1 = null

fn执行结束,其执行环境和所有变量应该随之销毁,但是当执行fn1的时候,输出fn内i的值,fn1没执行一次输出i自加一的结果。说明fn执行结束后,其执行环境相关连的变量对象并没有销毁,而是供fn1引用访问。如下图所示:

图片描述

以上就是闭包的原理,fn1作为外部函数,但是执行时仍然可以访问fn内的变量。

常见demo

1.异步函数调用传参,setTimeout 事件执行函数

function fn(a,b){
    return function(){ console.log(a,b) } } setTimeout(fn(1,2),1000) dom.addEventListener('click',fn(1,3),false)

2.解决异步导致的执行函数参数在执行时真实值不符合期望

var arr = []
for(var i =0; i < 3; i++){ arr[i] = function(i){ console.log(i) } } arr[0]() //3 arr[1]() //3 arr[2]() //3

改进,闭包+自执行函数实现i的顺序输出

var arr = []
for(var i =0; i < 3; i++){ arr[i] = (function(i){ return function(i){ console.log(i) } })(i) } arr[0]() //0 arr[1]() //1 arr[2]() //2

3.封装私有函数插件,避免命名冲突

var obj = (function(){
    var name = '111' var obj = { setName:function(){ name = '222' }, getName:function(){ return name } } return obj })() obj.setName() obj.getName() //222

外部无法访问name,只能通过obj对象访问name

闭包的优缺点

作用:

  1. 访问其他函数内部的变量
  2. 使变量常驻内存,在特定情况下实现共享(多个闭包函数访问同一个变量)
  3. 封装变量,避免命名冲突,多用在插件封装,闭包可以将不想暴漏的变量封装成私有变量

缺点:

  1. 当大量变量常驻内存时会导致内存耗费过量,影响速度,在ie9-版本会导致内存泄漏
  2. 不当使用闭包会导致被引用变量发生非预期变更

猜你喜欢

转载自www.cnblogs.com/wzp-monkey/p/9973398.html