在javascript异步编程、函数式编程中,有两个至关重要的技术callbackthis变量,又称之为回调当前对象上下文

一、星际迷航

javascript中的回调函数,我借用科幻小说的比喻,有点类似不同的宇宙空间。而且宇宙空间有两类:

  1. 一类就像从地球到火星,在代码上的表现是,在同一个时刻(帧)代码执行有严格的先后顺序。
  2. 另一类回调函数,像从当下去了天堂或冥界,跟现在下不属于同一个宇宙空间,代码在未来某一时刻才会进入。

而且这些宇宙空间还相可以互嵌套,简单理解可以用同步、异步函数来区别。

举个列子,先看看从地球到火星的旅行:

onLoad() {let array = ['1','2','3','4','5'];//过虑出数组中的奇数元素this._num = 2;array = array.map(function(i) {return parseInt(i);}).filter(function(i) {return i % this._num;}, this); //注意这里的this参数
}

上面代码中array对象上的map与filter中的匿名函数,就像两个小行星。onLoad外层就是地球,他们是在同一个时空之中,array中的元素像是做了一次星际旅行,断点会从上到下一句一句地执行。

Shawn对es6太过依赖,忍不住写了一行es6的等价代码:

//再看看es6的写法
array = array.map(i => parseInt(i)).filter(i => i % this._num);

这里解释一下,注意两点:

  1. 箭头函数中参数只有一个时,可以省略参数上的圆括号(arg)直接写成arg。
  2. 箭头函数中函数体只有一行代码,可以省略大扩号{}直接写表达示,同时将表达式的值默认为函数返回值,所以不需要写return。

再来看看Creator中常见的回调用法,在不同的宇宙空间的穿梭:

onLoad() {this._button.active = false;this.scheduleOnce(() => {this._button.active = true;}, 5);
}

使用scheduleOnce延时5秒显示_button节点,他与上面的map、filter函数不同的是异步执行。在调试中会发现断点在代码前后跳跃,断点前后跳跃不是关键,关键的是scheduleOnce函数他不会阻塞,不论scheduleOnce函数中的回调函数如何复杂都不会影响当前这一帧的运行效率。

在Creator中cc.loader.loadRes、cc.loader.load就是异步回调的,如果资源已经被加载过了,可以使用cc.loader.getRes通过函数返回值同步获取。理解同步与异步是编写javascript函数的重要心法,善于驾驭异步流程你就能在javascript中自由遨游,使用async.js来控制异步流程是一个高效的作法。

二、搞清楚this是谁

在纷繁复杂的星际旅行中,不论是同步还是异步,最为重要的是不要忘记“我是谁”。No不好意思,搞清楚我不重要,在你人生旅途中,要时间清醒,此刻的你到底是谁更重要。

对于javascript中的回调函数来说,函数中的this变量到底是谁,搞不清这个你很可能就会在旅行中回不来了,回到之前代码中的filter中的回调函数:

onLoad() {let array = ['1','2','3','4','5'];let array = [];let this._num = 2;array.filter(function(i) {return i % this._num;}, this); //<-----注意这里的this参数
}

filter的第二个参数this是用来改变回调函数中的this变量,如果不传这个this参数,里面的this._num访问就会有问题。

例如在Creator中有不少需要注册回调的API,后面都会紧跟一个target参数,target将来回调后的this变量。

this.node.on(cc.Node.EventType.TOUCH_START, this.memberFunction, this);

如果你不传入第三个参数this你的代码很可能会挂掉,函数的this上下文默认受调用者所控制。

//模拟一个组件中的点击事件
_onButtonTouchEnd() {//定义一个回调函数let callback = function() { cc.log(this);  //<----这个this是全局window}//执行回调函数,函数中的this是全局windowcallback();
}

上面代码callback中的this是全局window,这里我使用惯用方式总结了几个大招可以用来改变callback中的this变量。

三、星际巡航

javascript与c/c++、java等语言有个最大区别就是,函数中的this变量是可变的。几乎每个人都会在这一点栽跟头,这个特性既成就了javascript的高度灵活性,但也让不少初学者产生迷惑。改变js函数中this变量的技法我将其称之为:星际巡航术,为的是在迷航中认清自己。

第一式:凝神诀

Function.bind

javascript中所有的函数对象上都有bind方法,执行它将返回一个新的函数变量,这个返回的函数执行时的this上下文由bind的第一个参数所决定。看看在节点事件中的运用:

//去掉了第三个target参数
this.node.on(cc.Node.EventType.TOUCH_START, this.memberFunction.bind(this));

使用bind搞定,是不是很简单,我看好多人是这样做的。但请你思考一下那为什么Array.map、Array.filter、CreatorAPI要设计target参数呢?使用bind注册回调,容易踩到一个坑,稍后说明一下我的理解。我们再稍微深入一点,看看bind更多的用法:

//模拟一个组件中的点击事件
_onButtonTouchEnd() {//定义一个回调函数let callback = function(name, event) { cc.log(this);         //打印当前thiscc.log(name, event);  //打印参数 }//施展绑定诀,将callback中的this绑定为当前函数上下文中的thislet callback1 = callback.bind(this); //执行回调函数,函数中的this是曾经bind传入参数,这里就是当前组件对象callback1('button', 'touchEnd');//将callback中的this绑定为当前this上的_button节点对象let callback2 = callback.bind(this._button); //执行回调函数,函数中的this是bind传入的_button节点callback2('button', 'touchEnd');

凝神诀要义在于bind时的参数设定,就像是搓出一股波动拳,蓄而未发,“啊啰啰啰啰…”就是不“哽”出去。

而且bind函数还可以给函数传递参数,请仔细阅读下面代码:

 //定义一个回调函数let callback = function(arg1, arg2) { cc.log(this);         //打印当前thiscc.log(arguments)     //打印隐藏参数对象 cc.log(arg1, arg2);  //打印参数 }//绑定决还可以传入参数,传入的参与会排在原函数定义的参数之前let callback1 = callback.bind(this._button, 'button', 'touchEnd');//参数已经在bind时传入了,此时可以不用传入参数了callback1();//如果传入参数,调用时的参数会排在绑定时的参数后面callback1(1, 2); //参数顺序:['button', 'touchEnd', 1, 2]

将这股凝聚的能量任意流动(一系列的参数传递、变量赋值),在适合的地方释放出来,其中this变量与参数是由你之前精心设计的,这时会产生情人的效果,是一般静态语言难以做到的。

还需要特别的注意,每一股搓出的一股波动拳都是不同的函数对象。

let func1 = callback.bind(xxx);
let func2 = callback.bind(xxx);
//f1与f2是两个不同的函数对象
f1 === f2;  //返回false

这就是为什么在节点事件注册时使用bind容易掉入进的坑,当你想使用node.off你不能将之前事件回调给删除掉,这就是为什么要给你一个target参数的原因了。

不过Shawn还有更简单的办法注册事件,而且也不需要传入target,因为bind是es5时代的产物,es6有更好用的招数。

第二式:召唤诀

Function.call

你可能在想,Creator的API是如何利用target参数修改的回调中的this的呢?其实与Function.bind一样,javascript中所有的函数对象上都有一个call方法

//模拟一个组件中的点击事件
_onButtonTouchEnd() {//定义一个回调函数let callback = function(name, event) { cc.log(name, event); }//call的第一个参数是想变换的this上下文,后面为该函数的实际参数callback.call(this, 'button', 'touchEnd');
}

召唤诀的特点是:随喊随到,立即执行,其中最为重要的是call传入的第一个参数,就是你想变换的this变量,后面紧跟此函数的参数。

一个更有趣的实践hack一下Creator的cc.Button组件,做个神奇的勾子:

//先保存button状态切换函数
let updateState = cc.Button.prototype._updateState;
//自己写个函数来将他覆盖了
cc.Button.prototype._updateState = function () {//执行时的第一句,执行原来保存的_updateState,相当于执行基类函数//这里不能直接调用updateState,需要用call将内部this修正为当前buttonupdateState.call(this); if (this.node.interactable === this.interactable) {return;}//下面是根据是否禁用,设置button节点下的子节点变灰//做了条件判断只在不设置disabledSprite时生效this.node.interactable = this.interactable;if (this.enableAutoGrayEffect && this.transition !== cc.Button.Transition.COLOR) {if (!(this.transition === cc.Button.Transition.SPRITE && this.disabledSprite)) {this.node.children.forEach((node) => {let sprite = node.getComponent(cc.Sprite);if (sprite && sprite._sgNode) {sprite._sgNode.setState(this.interactable ? 0 : 1);}//原生平台退出 if(cc.sys.isNative) {return;}//Label的置灰实现目前只能在web端使用let label = node.getComponent(cc.Label);if (label && label._sgNode) {let shaderProgram = this.interactable ?cc.shaderCache.programForKey(cc.macro.SHADER_SPRITE_POSITION_TEXTURECOLOR) :cc.Scale9Sprite.WebGLRenderCmd._getGrayShaderProgram();label._sgNode._renderCmd.setShaderProgram(shaderProgram);}});}}
};

来看看演示效果:

Shawn还尝试了,将bind过的函数对象,再调用call,this任然是之前bind时的this不受call的第一个参数控制。

let func = callback.bind(xxx);
//执行时func函数的this任然是xxx,函数参数有效
func.call(yyy, arg1, arg2);

es5的时候call出现的频率是非常高的,但现在使用了es6除了做一些hack行为与面向对象的模拟外,大多数回调都可以用更加简单的一阳指可以搞定。

第三式:降龙诀

Function.apply

javascript中函数的参数变化无穷,参数个数可长可短(参数个数0~n),神鬼莫测,犹如一条游龙!降龙诀就是用来驯服这条善变的怪兽的!

_onButtonTouchEnd() {//定义一个回调函数,根据不同的参数个数有不同的处理let callback = function() { switch(arguments.lenght) {case 1:...break;case 2:...break;}}//call的第一个参数是想变换的this上下文,后面接一个数组参数callback.apply(this, ['button', 'touchEnd']);

同样的,所有函数上都有一个apply方法降龙诀的精髓有两点:

  1. 控制this上下文的变化,
  2. 可以将参数用一个数组打包进行传递,

函数执行任然是像普通调用一样,在平时用的地方不多,但在类的继承、执行基类函数、模拟面向对象等技术上是离不开它的。

第四式:一阳指

箭头函数 () => { … }

一阳指又称箭头函数,所指之处的函数this上下文,皆为当时调用时的this,看似平淡无其,实则威力巨大。

//模拟一个组件中的点击事件
_onButtonTouchEnd() {//定义一个箭头函数,当前this为组件对象let callback = (arg1, arg2) => { //此刻的this为定义函数时的this上下文对象cc.log(this);}callback(xxx, yyy);
}

凝神诀和召唤诀的运用大多数是为了修正匿名函数中的this为当前调用时的this,可显的有点啰哩叭嗦,一记一阳指轻松搞定!

在一阳指还没有被创造之前,使用的是闭包变量来做的:

var self = this;
function callback() {//使用self变量,指向调用时的this上下文cc.log(self);...
}
callback(xxx, yyy);

此方法也正是Bable编译器将es6转es5时生成的套路。

对于this的控制是凌波微步的内功基本详见《
英雄之舞—凌波微步》,如果运用的不好,就会如文中所讲的,强行走将起来,会造成经脉堵塞的危境!

#四、结束
最后总结一下我们介绍的招数

凝神诀—Function.bind
召唤诀—Function.call
降龙诀—Function.apply
一阳指—箭头函数

这些招数都是为了在回调函数中不要迷失this,或都说在回调中可以任意控制this。在javascript中函数是第一位的,函数可以动态生成,可以当参数传递,可以说javascript是披着c/c++的狼,骨子里其实是函数式编程语言。


欢迎关注「奎特尔星球」微信公众号,更期待您向公众号投稿,来我们一起成长!

星际巡航术—玩转javascript中this!相关推荐

  1. 星际巡航—玩转javascript中this!

    在javascript异步编程.函数式编程中,有两个至关重要的技术callback与this变量,又称之为回调与当前对象上下文. 一.星际迷航 javascript中的回调函数,我借用科幻小说的比喻, ...

  2. !! javascript_产量! 产量! 生成器如何在JavaScript中工作。

    !! javascript by Ashay Mandwarya ?️?? 由Ashay Mandwarya提供吗? 产量! 产量! 生成器如何在JavaScript中工作. (Yield! Yiel ...

  3. [推荐]在JavaScript中实现命名空间

    注:好久没写了,今天把我在公司内网写的文章拷一份,出来露露脸,刚转Web开发,所以开始学javascript! 在引入命名空间之前,一个令开发人员头疼的问题就是如何防止函数名/类名和其他人的冲突,在一 ...

  4. JavaScript 中回调地狱的今生前世

    1. 讲个笑话 JavaScript 是一门编程语言 2. 异步编程 JavaScript 由于某种原因是被设计为单线程的,同时由于 JavaScript 在设计之初是用于浏览器的 GUI 编程,这也 ...

  5. javascript函数式_如何以及为什么在现代JavaScript中使用函数式编程

    javascript函数式 by PALAKOLLU SRI MANIKANTA 通过PALAKOLLU SRI MANIKANTA In this article, you will get a d ...

  6. wpf绑定 dictionary 给定关键字不再字典中_为什么要在 JavaScript 中学习函数式编程?...

    请忘掉你认为你知道的有关 JavaScript 的任何东西,以初学者心态来接触这份资料. 为帮助你这样做,我们打算从头开始复习 JavaScript 的基础知识, 就好像你以前从来没有看到过 Java ...

  7. 在JavaScript中定义枚举的首选语法是什么? [关闭]

    在JavaScript中定义枚举的首选语法是什么? 就像是: my.namespace.ColorEnum = {RED : 0,GREEN : 1,BLUE : 2 }// later onif(c ...

  8. JavaScript中的“黑话”

    因为球是圆的,所以不论发生什么都有可能,对这点我是深信不疑的,但最近我总是在怀疑,JavaScript也是圆的! 什么是"黑话" 黑话,本指旧时江湖帮会人物的暗语.暗号,往往见于小 ...

  9. 在JavaScript中实现命名空间

    在引入命名空间之前,一个令开发人员头疼的问题就是如何防止函数名/类名和其他人的冲突,在一个公司内部项目组之间可以通过命名预定(比如加前缀等)解决这个问题,但是把视线放到整个软件开发领域,在当今协作开发 ...

最新文章

  1. 独家 | 集成学习入门介绍
  2. mac下安装mongodb
  3. 洛谷P3628 [APIO2010]特别行动队(斜率优化)
  4. python安装oracle驱动_Python安装Oracle数据库驱动
  5. Linux环境下搭建Tomcat+mysql+jdk
  6. 计算机专业Top20,全国计算机软件专业大学排名TOP20,清华居然不是第一!
  7. 分析“HTTP500内部服务器错误”解决方法
  8. c语言getch函数_在C / C ++中使用getch()函数
  9. 卡皇稳了,RTX3090获鲁大师Q1季度最强显卡!
  10. 一个计算机爱好者的不完整回忆(十二)下载软件
  11. 二进制漏洞挖掘之angr‘s Reaching Definition Analysis(一)
  12. android的权限一览表和RGB颜色对照表
  13. 怎么样把pdf压缩到最小
  14. 与贸易有关的知识产权协议 (转)
  15. 第八届蓝桥杯 java B组 日期问题
  16. 2015阿里校园招聘测试开发面试经验(广州站)
  17. Tableau表计算(2):计算依据
  18. 日本研制超音速小飞机或达音速2倍
  19. matlab计算后输出结果是分式(不是小数)
  20. 单面打印机打印小册子_如何在Microsoft Word中创建可打印的小册子

热门文章

  1. 已解决:excel求平均值时出现div/0 如何处理
  2. python正则表达式如何匹配“+”,“*”
  3. 湛蓝.Net代码生成器发布了
  4. python Numpy中array详解
  5. 和积法用MATLAB怎么做,权重确定方法归纳解读
  6. GitHub使用教程详解(下)——Git的安装以及Git命令详解
  7. 2020-2021 ACM-ICPC, Asia Seoul Regional Contest L. Two Buildings (决策单调性 分治)
  8. 我的全栈之路-C语言基础之C语言概述与开发环境搭建
  9. 计算机组网技术形考4单元答案,最新国家开放大学电大《计算机组网技术》网络核心课形考任务四答案...
  10. 02.创新与企业精神——有目的的创新和创新机遇的7个来源