JavaScript 面向对象之五:闭包与this指向

闭包

关于作用域的问题

var n = 999;
function f1() { 
    console.log(n);
}
f1() // 999

函数内部可以直接读取全局变量,函数 f1 可以读取全局变量 n。

但是,在函数外部无法读取函数内部声明的变量。

function f1() {
    var n = 99;
}
f1();
console.log(n);

有时我们却需要在函数外部访问函数内部的变量;
正常情况下,这是办不到的,只有通过变通方法才能实现。
那就是在函数的内部,再定义一个函数。

function f1() {
    var n = 999;

    var f2 = function() {
        console.log(n);
    } 
    return f2;
}
var f = f1();
f();

上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。
但是反过来就不行,f2内部的局部变量,对f1就是不可见的。

这就是JavaScript语言特有的”链式作用域”结构(chain scope),子级会一层一层地向上寻找所有父级的变量。
所以,父级的所有变量,对子级都是可见的,反之则不成立。

既然f2可以读取f1的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!

闭包就是函数f2,即能够读取其他函数内部变量的函数。
由于在JavaScript语言中,只有函数内部的子函数才能读取内部变量,

因此可以把闭包简单理解成“定义在一个函数内部的函数”
闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。
在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁;

闭包(closure)是 Javascript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

理解闭包,首先必须理解变量作用域。

关于JS垃圾回收机制的问题

function f1() { 
    var n = 99;
    console.log(++n);
}
f1(); //100 
f1(); //100

这里写图片描述
当我们在函数内部引入一个变量或函数时,系统都会开辟一块内存空间;还会将这块内存的引用计数器进行初始化,初始化值为0如果外部有全局变量或程序引用了这块空间,则引用计数器会自动进行+1操作当函数执行完毕后,变量计数器重新归零,系统会运行垃圾回收,将函数运行产生的数据销毁;如计数器不是 0 ,则不会清楚数据;

这个过程就称之为 “JS的垃圾回收机制” ;

如果将上节的代码,改为闭包形式:

function f1() {
    var n = 99;
    function f2(){
        console.log(++n);
    }
    return f2; 
}
var f = f1();
f(); //100
f(); //101

运行代码发现,函数调用一次,其变量 n 变化一次;

因 函数f1被调用时,返回的结果是f2函数体,也就是说,f2函数被当作值返回给f1的调用者,
但是f2函数并没有在此时被调用执行,
所以整个 f1 函数体,无法判断子函数f2会对其产生何种影响,无法判断 变量n是否会被使用;
即使f1函数被调用结束,整个f1函数始终保留在内存中,不会被垃圾回收机制回收;

闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,
即闭包可以使得它诞生环境一直存在;

注意,外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。

因此不能滥用闭包,否则会造成网页的性能问题。

闭包小案例:缓存随机数

需求:获取随机数,随机数一旦生成,会在程序中多次使用,所以,在多次使用中不变;

function f1(){
    var num = parseInt(Math.random()*100)+1;
    return function (){
        console.log(num);
    }
}
var f2 = f1();
f2();
f2();
f2();

闭包点赞案例:

<ul>
    <li><img src="images/ly.jpg" alt=""><br/><input type="button" value="赞(1)"></li>
    <li><img src="images/lyml.jpg" alt=""><br/><input type="button" value="赞(1)"></li>
    <li><img src="images/fj.jpg" alt=""><br/><input type="button" value="赞(1)"></li>
    <li><img src="images/bd.jpg" alt=""><br/><input type="button" value="赞(1)"></li>
</ul>
<script>
    function f1() {
        var value=1;
        return function () {
            this.value="赞("+(++value)+")";
        }
    }
    //获取所有的按钮
    var btnObjs=document.getElementsByTagName("input");
    for(var i=0;i<btnObjs.length;i++){
        var ff=f1();
        //将 闭包函数 绑定给事件
        btnObjs[i].onclick=ff;
    }
</script>

思考以下两段代码的运行结果:

var name = "The Window";
var object = {
    name: "My Object",
    getNameFunc: function () {
        return function () {
            console.log(this.name);
        };
    }
};

object.getNameFunc()() 
var name = "The Window";  
var object = {    
    name: "My Object",
    getNameFunc: function () {
        var that = this;
        return function () {
            console.log(that.name);
        };
    }
};
object.getNameFunc()(); 

通过分析代码得知,其中的 this 是关键,那么this到底是什么?

this 到底是谁

this的不同指向

this关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义,大部分开发任务都无法完成。

记住一点:this不管在什么地方使用:它永远指向一个对象

下面是一个实际的例子。

var person = {
    name: '张三',
    describe: function () {
        console.log('姓名:'+ this.name);
    }
};

person.describe()
// "姓名:张三"

上面代码中,this.name表示name属性所在的那个对象。由于this.namedescribe方法中调用,而describe方法所在的当前对象是person,因此this指向personthis.name就是person.name

由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即this的指向是可变的。

var A = {
    name: '张三',
    describe: function () {
        console.log('姓名:'+ this.name);
    }
};

var B = {
    name: '李四'
};

B.describe = A.describe;
B.describe()
// "姓名:李四"

上面代码中,A.describe属性被赋给B,于是B.describe就表示describe方法所在的当前对象B,所以this.name就指向B.name

稍稍重构这个例子,this的动态指向就能看得更清楚。

function f() {
    console.log('姓名:'+ this.name);
}

var A = {
    name: '张三',
    describe: f
};

var B = {
    name: '李四',
    describe: f
};

A.describe() // "姓名:张三"
B.describe() // "姓名:李四"

上面代码中,函数f内部使用了this关键字,随着f所在的对象不同,this的指向也不同。

只要函数被赋给另一个变量,this的指向就会变。

var A = {
    name: '张三',
    describe: function () {
        console.log('姓名:'+ this.name);
    }
};

var name = '李四';
var f = A.describe;
f(); // "姓名:李四"

上面代码中,A.describe被赋值给变量f,的内部this就会指向f运行时所在的对象(本例是顶层对象)。

案例:判断数值合法性

<input type="text" name="age" size=3 onChange="validate(this, 10, 20);">

<script>
function validate(obj, x, y){
    if ((obj.value < x) || (obj.value > y)){
        console.log('合法');
    }else{
        console.log('不合法');
    }
}
</script>

上面代码是一个文本输入框,每当用户输入一个值,鼠标离开焦点就会触发onChange事件,并执行validate回调函数,验证这个值是否在指定范围。浏览器会向回调函数传入当前对象,因此this就代表传入当前对象(即文本框),就然后可以从this.value上面读到用户输入的值。

JavaScript语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象下运行的,this就是函数运行时所在的对象(环境)。这本来并不会让我们糊涂,但是JavaScript支持运行环境动态切换,也就是说,this的指向是动态的,没有办法事先确定到底指向哪个对象,这才是最初初学者感到困惑的地方。

使用场合

(1)全局环境

全局环境使用this,它指的就是顶层对象window

this === window // true

function f() {
  console.log(this === window);
}
f() // true

(2)构造函数

构造函数中的this,指的是实例对象。

function F(){
    this.hello=function(){
        console.log('Hello'+this.name);
    }
}
var f1 = new F();
f1.name = '张三';
f1.hello();


var f2 = new F();
f2.name = '刘能';
f2.hello();

(3)对象的方法

方法在哪个对象下,this就指向哪个对象。

var o1 = {
    s1:'123',
    f1:function (){
        console.log(this.s1)
    }
}

var o2 = {
    s1:'456',
    f1:o1.f1
}

o2.f1();
(4)定时器中的this
    // 第一个参数 有两种写法  
    // 1:可执行的JS代码(字符串)
    // 2:函数

    var age = 20;
    var o = {
        age:18,
        gets:function(){
            console.log(this.age);
        }
    }

    var l = o.gets;
    setInterval(function(){},2000); 
    setInterval('o.gets',2000); //  
    setInterval('o.gets()',2000); //18
    setInterval(undefined,2000); // 

使用this时的注意事项

(1) 避免包含多层this

var o = {
    f1: function () {
        console.log(this);
        var f2 = function () {
            console.log(this);
        }
        f2();
    }
}

o.f1()
// Object  
// Window  

如果要在内层函数中使用外层的this指向,一般的做法是:

var o = {
    f1: function () {
        console.log(this);
        var that = this;
        var f2 = function () {
            console.log(that);
        }
        f2();
    }
}

o.f1()
// Object

(2)不在循环数组中使用this

var ar = ['a','b','c'];
ar.forEach(function(v,k,ar){
    console.log(this[k])
})

this的动态切换,固然为JavaScript创造了巨大的灵活性,但也使得编程变得困难和模糊。

有时,需要把this固定下来,避免出现意想不到的情况;JavaScript提供了callapplybind这三个方法,来切换/固定this的指向。

call()方法、apply()方法、bind()方法

call()方法

var lisi = {names:'lisi'};
var zs = {names:'zhangsan'};
function f(age){
    console.log(this.names);
    console.log(age);

}
f(23);//undefined
f.call(zs,32);//zhangsan

call方法使用的语法规则

函数名称.call(obj,arg1,arg2…argN);

参数说明:

obj:函数内this要指向的对象,

arg1,arg2…argN :参数列表,参数与参数之间使用一个逗号隔开

apply()方法

函数名称.apply(obj,[arg1,arg2…,argN])

参数说明:

obj :this要指向的对象

[arg1,arg2…argN] : 参数列表,但是要求格式为数组

var lisi = {name:'lisi'}; 
var zs = {name:'zhangsan'}; 
function f(age,sex){
    console.log(this.name+age+sex); 
}
f.apply(zs,[23,'nan']);

bind()方法

bind方法用于将函数体内的this绑定到具体的某个对象上

this.x = 9; 
var module = {
    x: 81,
    getX: function() {
        return this.x;
    }
};

module.getX(); // 返回 81

var retrieveX = module.getX;
retrieveX(); // 返回 9, 在这种情况下,"this"指向全局作用域

// 创建一个新函数,将"this"绑定到module对象
// 新手可能会被全局的x变量和module里的属性x所迷惑
var boundGetX = retrieveX.bind(module);
boundGetX(); // 返回 81

猜你喜欢

转载自blog.csdn.net/github_27314097/article/details/81904591