为什么函数赋值为null后,后续还会执行?

<button id="btn">btn</button>
function log () {
   console.log('111')
}

let btn = document.querySelector('#btn')

btn.addEventListener('click', log)

btn.addEventListener('click', function () {
   console.log(log)  
   log = null
   console.log(log)
})

第一次点击输出:
111
log函数
null

后续点击输出:
111
null
null

问题?
为什么log函数赋值为null后,后续点击还会执行?

阅读 6.5k
8 个回答

这个其实就是个值传递和引用传递的问题,前面两个哥们说得很有意思,但是我觉得都没说到点子上。

JS 中基本数据类型

基本的数据类型有:undefined,boolean,number,string,null。 基本类型存放在栈区,访问是按值访问的,就是说你可以操作保存在变量中的实际的值。

也就是说,这种赋值类似于拷贝了一份。a和b在 var b = a;后就再也没有关系了。互不影响。

引用赋值

引用类型指的是对象:js中的arrayobject。可以拥有属性和方法,并且我们可以修改其属性和方法。引用对象存放的方式是:在栈中存放变量名(该变量的值是堆中真实数据的指针(对象在堆中的存放地址)),在堆中存放数据(真正的数据)。

对象使用的是引用赋值。当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在堆中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。

JS 函数是值传递

JS是值传递,那么传递引用类型和基础类型都是值传递,都是拷贝一份,但是引用类型变量实际上存的是对象/数组的指针,所以对于引用类型的传递,函数调用时候产生的实参是相当于变量的指针拷贝。另外对于函数来说,定义函数的时候,函数名实际存放的是具体函数的一个指针,参考上面关于堆栈存储的说明。

用你这个例子来说明:

function log () {
   console.log('111')
}

let btn = document.querySelector('#btn')

// 这里btn.addEventListener调用时候第二个参数传入的实际上是函数 log 的指针拷贝,
// 指向log这个函数。
btn.addEventListener('click', log)

btn.addEventListener('click', function () {
   console.log(log)  
   // log其实存的是一个函数指针,存在栈中,指向堆中的具体定义的这个函数,
   // 此处设置为null,相当于把log的指针置为null
   // 实际上具体的函数还存在堆中
   log = null 
   console.log(log)
})

所以你看,产生上面的结果就很容易理解了吧?

换个写法就另当别论了:

btn.addEventListener('click', function(...args) {
  log(...args);
});

看起来好比某人被骗进黑煤窑了,家里人找不到他,就申请把他户口注销了,但注销并不影响他继续挖煤。

log是一个变量,他的值是内存中的函数fn,addEventListener绑定了一个事件,回调函数为log,其实是内存中的函数fn,现在把log变为null,但是内存中的函数fn还是存在的,因为事件回调还在引用他,函数回调也能通过这种形式解绑,需要使用removeEventListener

函数名是指针。

function fn() {console.log('test')}
var callback = fn;
fn = null;
callback(); // test

// 在你的的例子中
btn.addEventListener('click', log)
// 相当于创建了一个局部变量log,与原来的变量log指向同一堆内存地址引用(函数对象)

感觉和这个例子是相仿的,即使obj = null,但是里面的定时器依然会触发

const obj = {
    alert:setInterval(()=> {
        console.log('123')
    },1000)

}
obj = null

这个其实是你理解上有些偏差,注意

btn.addEventListener('click', log)

btn.addEventListener('click', function () {
   console.log(log)  
   log = null
   console.log(log)
})

中,你的btn按钮的click事件处理先是被添加了一开始log函数(所指的处理过程),后又添加了个匿名函数处理过程,所以实质上其对应的处理过程为

function(){ 
console.log('111')
}
function(){
console.log(log)
log=null
console.log(log)
}

在第一次运行时,log开始是log函数定义,后面变成了null。
你想取消btn的处理,其实应该是采用btnremoveEventListener来移除,注意,removeEventListener只能移除外部有名函数处理,匿名的不能简单移除。

举个例子

你的手上涂满了红色的染料,我碰了一下你的手,我也粘上了红色染料,你将手上的红色染料洗掉了,那我手上的红色染料就会消失么,显然不会。

addEventListener是将log的值绑定到了click函数上,并不是将log绑定上了。就好比我手上的红色是染料,而不是我的手换成了你的手。

因为java只有一种传递参数的方式:值传递。
在值传递中,实参的值被传给形参,方法体内对形参的任何赋值操作都不会影响到实参。

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题