Fork me on GitHub

对js的面向对象以及闭包的认识

前言

本文主要针对JavaScript的几种设计模式以及原型链、闭包这几个知识点进行学习和了解…

面向对象

几种模式

工厂模式

  • 接收参数,内部构造一个对象并return
1
2
3
4
5
6
7
8
9
function Function1(args){
var obj = new Object();
obj.args = args;
//添加方法
obj.littleFuction = function() {

}
return obj;
}

构造函数模式

特点:

  • 不显式的创建对象,直接将方法、属性赋给this对象
  • 不return
  • 需要实例化(new)
  • 每个成员无法得到复用

Tips: 任何函数如果通过new操作符来调用,就可以作为构造函数;如果不用new操作符,则当普通函数(this则指向Global对象,浏览器中即window对象)

1
2
3
4
5
6
7
8
9
function Function2(args) {
this.args = args;
//添加方法
this.littleFuction = function() {

}
}
//使用
var test = new Function2(args);

原型模式

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

constructor属性:在自定义构造函数后,原型对象会默认取得改属性,constructor是指向prototype属性所在函数的指针。

Tips:

  • 对象实例添加与原型对象同名属性时,不能重写,只会覆盖;delete操作符可删除实例属性。
  • 原型具有动态性,实例的创建和原型之间是松散的,即先创建实例后修改原型也ok。随时可为原型添加属性和方法。
    • 特别注意:如果重写原型对象(用对象字面量重写——object.prototype = { }),会切断现有原型与任何之前已经存在实例之间的联系。即原先new的实例无法引用的还是最初的原型。

原型对象存在的问题:

  • 所有实例共享属性和方法,对于引用类型值就不可独立开来,容易出现问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Function3() {
}
//给原型添加属性
Function3.prototype.args = new_args;
//给原型添加方法
Function3.prototype.littleFuction = function() {
alert(this.args);//this指向的就是原型
}
//创建实例
var instance = new Function3();

instance.args = another_args;//只是这个实例的属性会覆盖掉原型的属性

instance.prototype = {
agrs: another_args
}//这样则会重写原型属性

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

  • 构造函数模式用于定义实例属性
  • 原型模式用于定义方法和共享的属性
1
2
3
4
5
6
7
8
9
10
11
12
//构造函数模式
function Function4(args) {
//定义实例属性
this.args = args;
}
//原型模式
Function4.prototype = {
share_args: share_args;
littleFuction : function() {
alert(this.args);
}
}

动态原型模式

通过构造函数来初始化原型。

通过 if 语句判断在调用构造方法时,参数是否有传某个方法,来决定是否需要初始化原型(就是采用原型模式来添加方法或属性)。

寄生构造函数模式

工厂模式 + new 操作符实例对象,调用构造函数(其实就是工厂模式的函数)

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

稳妥构造函数模式

与寄生构造函数模式类似,不同之处在于:

  • 新创建的对象的实例方法不引用this
  • 不使用new调用构造函数

其实就是函数传进来的值无法直接访问,因为没有了this和new,没有引用指向这些值。

想访问值可以添加方法,类似java的getter和setter

优点:数据比较安全

继承

ES支持实现继承,不支持接口继承。而实现继承主要靠原型链实现

原型链

何为原型链?

  • 其实就是让原型对象包含的指向构造函数的内部指针,改为指向另一个原型对象,从而层层递进,形成原型链

  • 代码上实现就是将 子类原型的prototype指针 指向 超类原型的实例

    1
    2
    3
    4
    5
    6
    7
    8
    //超类原型
    function SuperType() {
    }
    //子类原型
    function SubType() {
    }
    //继承 SuperType
    SubType.prototype = new SuperType();

事实上,所有的函数默认原型都是Object的实例,所以默认原型都会包含一个内部指针,指向Object.prototype。

我们也可以知道访问实例属性的本质:一步一步往上直至原型链末端搜索查找

Tips

  • 给原型添加方法一定要放在替换原型的语句之后。即超原型的实例赋给子原型后,再添加(重写)子原型或超原型方法
  • 通过原型链实现继承时,不能使用对象字面量创建原型方法。
  • 在创建子类型的实例时,不能向超类型的构造函数中传递参数

借用构造函数

在子类型构造函数内部调用超类型

代码上实现:超类型函数调用 call(this, args) 或 apply(this, args) 方法。可传参数,意味着相对原型链有个很大的优势,可以在子类构造函数中向超类构造函数传递参数

缺点:方法都在构造函数中定义,无法函数复用。

1
2
3
4
5
6
7
8
//超类原型
function SuperType() {
}
//子类原型
function SubType() {
//继承 SuperType
SuperType.call(this);
}

组合继承

实质就是把原型链和借用构造函数组合使用

Tips:

  • 无论什么情况下都会调用两次超类型函数
    • 创建子类型原型时
    • 子类型构造函数内部调用 call() 或 apply() 方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//超类原型
function SuperType(args) {
this.args = args;
}
//子类原型
function SubType(args, another_args) {
//继承 SuperType 的属性
SuperType.call(this, args);
//也能拥有自己的属性
this.another_args = another_args;
}
//继承 SuperType
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

原型式继承

可以在不必预先定义构造函数的情况下实现继承,本质是执行给定对象的浅复制。复制得到的副本可以进一步改造。

必须要有一个对象作为另一个对象的基础。

1
2
3
4
5
var object = {
agrs: "args";
}
var another_object = Object.create(object);//create方法是es5新增的
var another_object = Object(object);//或者这样

可以做到让一个对象与另一个对象保持类似

寄生式继承

基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。

1
2
3
4
5
6
7
8
9
function creatObject(original) {
var clone = Object(original);
//可接着添加属性或方法,属于自己的,与源对象独立的
return clone;
}
var object = {
agrs: "args";
}
var another_object = creatObject.create(object);

与寄生构造函数和工厂模式类似

寄生组合式继承

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

1
2
3
4
5
function inheritPrototype(subType, superType) {
var prototype = object(superType.prototype);//创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
}

寄生组合式继承是实现基于类型继承的最有效方式


函数与闭包

js 定义函数的形式有两种

  • 函数声明
  • 函数表达式

了解函数声明提升

​ 函数声明提升:在执行代码前会先读取函数声明。即代码顺序可以先调用再声明。但只对函数声明有效,对函数表达式则不行。

对于函数表达式(匿名函数),使用前必须先赋值。

变量作用域

  • 函数内部可以访问外部变量,而函数外部无法读取函数的局部变量
  • 没有用var声明的变量,默认为全局变量

每个函数被调用时,会创建他的执行环境,且执行环境有相应的作用域链,作用域链本质上是一个指向变量对象的指针列表,只引用不包含变量对象。而变量对象又有本地活动对象和全局变量对象之分。

匿名函数的执行环境具有全局性

何为闭包?

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

可以把闭包简单理解成”定义在一个函数内部的函数”。

1
2
3
4
5
6
function f1() {
var args = 'hello';
return function() {
alert(args);
}
}

函数 f1 返回的匿名函数就是闭包。

需要注意的是,匿名函数从 f1() 被返回后,它的作用域是包含f1的局部活动对象和全局活动对象的。而且,f1()函数执行完后,其执行环境的作用域链会被销毁,但它的活动对象依然留在内存中,只有直到匿名函数被销毁它才会销毁。

1
2
3
4
5
var res = f1();//创建函数

var res2 = res();//调用函数(匿名函数)

res = null;//解除对匿名函数的引用,以便垃圾回收机制回收,释放内存

Tips:

  • 前面说过,闭包会引用包含函数的整个活动对象,也就是说闭包会使得函数中的变量都被保存在内存中,这样内存消耗很大,因此不能滥用闭包。

  • 匿名函数的this对象通常指向window(执行环境具有全局性),但要视具体情况。下面是书上的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    var name = "The Window";
    //第一个例子
      var object = {
        name : "My Object",

        getNameFunc : function(){
          return function(){
            return this.name;
          };
        }
      };
      alert(object.getNameFunc()());//"The Window"

    //第二个例子
      var object = {
        name : "My Object",

        getNameFunc : function(){
          var that = this;
          return function(){
            return that.name;
          };
        }
      };
      alert(object.getNameFunc()());//"My Object"
    • 第一个例子:调用 getNameFuncth 后,返回匿名函数。因为每个函数被调用时,会自动取得 thisargument 两个特殊变量。而内部函数搜索这两个变量时,只会搜索到其活动对象为止。因此访问不到外部函数中的 thisargument
    • 第二个例子:因为把 this 赋值给 that ,函数返回后,that 仍然引用着 object ,所以闭包可以访问。

闭包的用途

模仿块级作用域

js本身没有块级作用域的概念,可以用匿名函数模仿

  1. 创建并立即调用一个函数
  2. 执行完,不会在内存中留下对该函数的引用,函数内部的所有变量都会被立即销毁(除了那些被赋值给外部作用域的变量)。
1
2
3
4
5
6
7
8
9
10
//第一种
var blockFunction = function() {
//块级作用域
}
blockFunction();//立即调用

//第二种 函数表达式
(function() {
//块级作用域
})();

在对象中创建私有变量

相关概念:

  • 特权方法:有权访问私有变量和私有函数的公有方法。

在函数内部创建闭包,可以利用闭包创建特权方法。

有如下两种方法:

  • 第一种,在构造函数中定义特权方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function MyFunction() {
//私有变量
var privateVariable = 1;
//私有函数
function privateFunction() {
return "I'm a private function"
}
//特权方法 其实就是个闭包
this.publicMethod = function() {
//访问私有变量
privateVariable++;
//返回私有函数
return privateFunction;
}
}

​ 优点:可以做到私有变量只能通过特权方法才能访问和修改。

​ 缺点:因为采用构造函数模式,对每个实例都会创建同样一组新方法。

  • 第二种,使用原型模式,在私有作用域中定义私有变量或函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
(function() {
//私有变量
var privateVariable = 1;
//私有函数
function privateFunction() {
return "I'm a private function"
}
//构造函数 没有用var声明,会变成全局变量,但严格模式下会报错
MyFunction() {
};
//特权方法
MyFunction。prototype.publicMethod = function() {
//访问私有变量
privateVariable++;
//返回私有函数
return privateFunction;
}
})();

​ 优点:私有变量和私有函数所有实例共享。

​ 缺点:每个实例没有自己的私有变量。

使用模块模式为单例创建私有变量和特权方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var singleton = function() {
//私有变量
var privateVariable = 1;
//私有函数
function privateFunction() {
return "I'm a private function"
}
//返回对象的匿名函数
return {
//公有属性
publicProperty: "I'm a public property"
//特权方法
publicMethod : function() {
//访问私有变量
privateVariable++;
//返回私有函数
return privateFunction;
}
}
}();

使用场景:必须创建一个对象并以某些数据对其进行初始化,同时还需要公开一些能够访问私有数据的方法。

增强的模块模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var singleton = function() {
//私有变量
var privateVariable = 1;
//私有函数
function privateFunction() {
return "I'm a private function"
}
//创建对象,是某种类型(xxx)的实例
var object = new xxx();

//公有属性
object.publicProperty = "I'm a public property"
//特权方法
object.publicMethod = function() {
//访问私有变量
privateVariable++;
//返回私有函数
return privateFunction;
};
//返回该对象
return object;
}();

这种模式适用于单例必须是给定某种类型的实例。

-------------Page's overThanks for reading-------------

本文标题:对js的面向对象以及闭包的认识

本文作者:SherlockYang

发布时间:2019年07月07日 - 14:07

最后更新:2019年10月11日 - 23:10

原始链接:https://sherlock-y.com/2019/07/07/对js的面向对象以及闭包的学习/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

如果文章内容对你有帮助,就赏博主一瓶维他奶吧~