JavaScript, 5 ways to call a function

本文探讨了JavaScript中五种不同的函数调用方式,包括全局函数调用、方法调用、使用apply和call改变this指向以及构造函数调用。通过具体实例说明了每种调用方式下this关键字的行为差异。
JavaScript, 5 ways to call a function
This post is part of a series called JavaScript Demystified.

Time after time I find JavaScript code that has bugs caused by lack of proper understanding of how functions work in JavaScript (a lot of that code has been written by me, by the way.) JavaScript has functional programming characteristics, and that can get in our way until we decide to face and learn it.

For starters, let's examine five ways to invoke a function. On the surface we might be tempted to think that functions work exactly like C#, but we will see that there are important differences and ignoring them will undoubtedly result in hard to track bugs.

Let's first create a simple function that we will be using through the rest of this post. This function will just return an array with the current value of this and the two supplied arguments.

<script type="text/javascript">
function makeArray(arg1, arg2){
	return [ this, arg1, arg2 ];
}
</script>

Most common way, unfortunately, global function calls

When we are learning JavaScript we learn how to define functions using the syntax used in the example above. We learn that it's also very easy to call that function — all we need to do is:

makeArray('one', 'two');
// => [ window, 'one', 'two' ]

Wait a minute. What's that window object doing there? Why is it the value of this? If you haven't stopped to think about it, please stay with me here.

In JavaScript, and I'm not talking specifically about the browser here, there's a default/global object. It's as if every code that we write which seems to be just "loose" inside your script (i.e. outside of any object declaration) is actually being written in the context of that global object. In our case, that makeArray function isn't just a loose "global" function, it's a method of the global object. Bringing ourselves back to the browser, the global object is mapped to the window object in this environment. Let's prove that.

alert( typeof window.methodThatDoesntExist );
// => undefined
alert( typeof window.makeArray);
// => function

What all this means is that calling makeArray like we did before is the same as calling as follows.

window.makeArray('one', 'two');
// => [ window, 'one', 'two' ]

I say it's unfortunate that this is the most common way because it leads us to declare our functions globally by default. And we all know that global members are not exactly the best practice in software programming. This is especially true in JavaScript. Avoid globals in JavaScript, you won't regret it.

JavaScript function invocation rule #1 In a function called directly without an explicit owner object, like myFunction(), causes the value of this to be the default object ( window in the browser).

Method call

Let's now create a small object and use the makeArray function as one of its methods. We will declare the object using the literal notation. Let's also call this method.

//creating the object
var arrayMaker = {
	someProperty: 'some value here',
	make: makeArray
};

//invoke the make() method
arrayMaker.make('one', 'two');
// => [ arrayMaker, 'one', 'two' ]
// alternative syntax, using square brackets
arrayMaker['make']('one', 'two');
// => [ arrayMaker, 'one', 'two' ]

See the difference here? The value of this became the object itself. You may be wondering why isn't it still window since that's how the original function had been defined. Well, that's just the way functions are passed around in JavaScript. Function is a standard data type in JavaScript, an object indeed; you can pass them around and copy them. It's as if the entire function with argument list and body was copied and assigned to make in arrayMaker. It's just like defining arrayMaker like this:

var arrayMaker = {
	someProperty: 'some value here',
	make: function (arg1, arg2) {
		return [ this, arg1, arg2 ];
	}
};
JavaScript function invocation rule #2 In a function called using the method invocation syntax, like obj.myFunction() or obj['myFunction'](), causes the value of this to be obj.

This is a major source of bugs in event handling code. Look at these examples.

<input type="button" value="Button 1" id="btn1"  />
<input type="button" value="Button 2" id="btn2"  />
<input type="button" value="Button 3" id="btn3"  οnclick="buttonClicked();"/>

<script type="text/javascript">
function buttonClicked(){
	var text = (this === window) ? 'window' : this.id;
	alert( text );
}
var button1 = document.getElementById('btn1');
var button2 = document.getElementById('btn2');

button1.onclick = buttonClicked;
button2.onclick = function(){   buttonClicked();   };
</script>

Clicking the first button will display "btn1" because it's a method invocation and this will be assigned the owner object (the button input element.) Clicking the second button will display "window" because buttonClicked is being called directly (i.e. not like obj.buttonClicked().) This is the same thing that happens when we assign the event handler directly in the element's tag, as we have done for the third button. Clicking the third button does the same of the second button.

That's another advantage of using a library like jQuery. When defining event handlers in jQuery, the library will take care of overriding the value of this and make sure it contains a reference to the element that was the source of the event.

//using jQuery
$('#btn1').click( function() {
	alert( this.id ); // jQuery ensures 'this' will be the button
});

How does jQuery override the value of this? Keep reading.

Two more: apply() and call()

The more you leverage functions in JavaScript, the more you find yourself passing functions around and needing to invoke them in different contexts. Just like jQuery does in the event handler functions, you'll often need to override the value of this. Remember I told you functions are objects in JavaScript? Functions have predefined methods, two of them are apply() and call(). We can use them to do precisely that kind of overriding.

var gasGuzzler = { year: 2008, model: 'Dodge Bailout' };
makeArray.apply( gasGuzzler, [ 'one', 'two' ] );
// => [ gasGuzzler, 'one' , 'two' ]
makeArray.call( gasGuzzler,  'one', 'two' );
// => [ gasGuzzler, 'one' , 'two' ]

The two methods are similar. The first parameter will override this. They differ on the subsequent arguments. Function.apply() takes an array of values that will be passed as arguments to the function and Function.call() takes the same arguments separately. In practice I believe you'll find that apply() is more convenient in most cases.

JavaScript function invocation rule #3 If we want to override the value of this without copying the function to another object, we can use myFunction.apply( obj ) or myFunction.call( obj ).

Constructors

I won't delve into the details of defining types in JavaScript but at minimum we should be aware that there aren't classes in JavaScript and that any custom type needs a constructor function. It's also a good idea to define the methods of your type using the prototype object, which is a property of the constructor function. Let's create a small type.

//declaring the constructor
function ArrayMaker(arg1, arg2) {
	this.someProperty = 'whatever';
	this.theArray = [ this, arg1, arg2 ];
}
// declaring instance methods
ArrayMaker.prototype = {
	someMethod: function () {
		alert( 'someMethod called');
	},
	getArray: function () {
		return this.theArray;
	}
};

var am = new ArrayMaker( 'one', 'two' );
var other = new ArrayMaker( 'first', 'second' );

am.getArray();
// => [ am, 'one' , 'two' ]

What's very important to note here is the presence of the new operator before the function call. Without that your function will just be called like a global function and those properties that we are creating would be created on the global object (window.) And you don't want to do that. Another issue is that, because you typically don't have an explicit return value in your constructor function, you'll end up assigning undefined to some variable if you forget to use new. For these reasons it's a good convention to name your constructor functions starting with an upper case character. This should serve as a reminder to put the new operator before the call.

With that taken care of, the code inside the constructor is very similar to any constructor you probably have written in other languages. The value of this will be the new object that you are trying to initialize.

JavaScript function invocation rule #4 When used as a constructor, like new MyFunction(), the value of this will be a brand new object provided by the JavaScript runtime. If we don't explictly return anything from that function, this will be considered its return value.

It's a wrap

I hope understanding the differences between the invocation styles help you keeping bugs out of your JavaScript code. Some of these bugs can be very tricky do identify and making sure you always know what the value of this will be is a good start to avoiding them in the first place.

attach: function (jid, sid, rid, callback) { this.jid = jid; this.sid = sid; this.rid = rid; this.connect_callback = callback; this.domain = Strophe.getDomainFromJid(this.jid); this.authenticated = true; this.connected = true; }, /** Function: rawInput * User overrideable function that receives raw data coming into the * connection. * * The default function does nothing. User code can override this with * > Strophe.Connection.rawInput = function (data) { * > (user code) * > }; * * Parameters: * (String) data - The data received by the connection. */ rawInput: function (data) { return; }, /** Function: rawOutput * User overrideable function that receives raw data sent to the * connection. * * The default function does nothing. User code can override this with * > Strophe.Connection.rawOutput = function (data) { * > (user code) * > }; * * Parameters: * (String) data - The data sent by the connection. */ rawOutput: function (data) { return; }, /** Function: send * Send a stanza. * * This function is called to push data onto the send queue to * go out over the wire. Whenever a request is sent to the BOSH * server, all pending data is sent and the queue is flushed. * * Parameters: * (XMLElement) elem - The stanza to send. */ send: function (elem) { if (elem !== null && typeof(elem["sort"]) == "function") { for (var i = 0; i < elem.length; i++) { this._data.push(elem[i]); } } else { this._data.push(elem); } this._throttledRequestHandler(); clearTimeout(this._idleTimeout); this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); }, /** PrivateFunction: _sendRestart * Send an xmpp:restart stanza. */ _sendRestart: function () { this._data.push("restart"); this._throttledRequestHandler(); clearTimeout(this._idleTimeout); this._idleTimeout = setTimeout(this._onIdle.bind(this), 100); }, /** Function: addTimedHandler * Add a timed handler to the connection. * * This function adds a timed handler. The provided handler will * be called every period milliseconds until it returns false, * the connection is terminated, or the handler is removed. Handlers * that wish to continue being invoked should return true. * * Because of method binding it is necessary to save the result of * this function if you wish to remove a handler with * deleteTimedHandler(). * * Note that user handlers are not active until authentication is * successful. * * Parameters: * (Integer) period - The period of the handler. * (Function) handler - The callback function. * * Returns: * A reference to the handler that can be used to remove it. */ addTimedHandler: function (period, handler) { var thand = new Strophe.TimedHandler(period, handler); this.addTimeds.push(thand); return thand; }, /** Function: deleteTimedHandler * Delete a timed handler for a connection. * * This function removes a timed handler from the connection. The * handRef parameter is *not* the function passed to addTimedHandler(), * but is the reference returned from addTimedHandler(). * * Parameters: * (Strophe.TimedHandler) handRef - The handler reference. */ deleteTimedHandler: function (handRef) { // this must be done in the Idle loop so that we don't change // the handlers during iteration this.removeTimeds.push(handRef); }, /** Function: addHandler * Add a stanza handler for the connection. * * This function adds a stanza handler to the connection. The * handler callback will be called for any stanza that matches * the parameters. Note that if multiple parameters are supplied, * they must all match for the handler to be invoked. * * The handler will receive the stanza that triggered it as its argument. * The handler should return true if it is to be invoked again; * returning false will remove the handler after it returns. * * As a convenience, the ns parameters applies to the top level element * and also any of its immediate children. This is primarily to make * matching /iq/query elements easy. * * The return value should be saved if you wish to remove the handler * with deleteHandler(). * * Parameters: * (Function) handler - The user callback. * (String) ns - The namespace to match. * (String) name - The stanza name to match. * (String) type - The stanza type attribute to match. * (String) id - The stanza id attribute to match. * (String) from - The stanza from attribute to match. * * Returns: * A reference to the handler that can be used to remove it. */ addHandler: function (handler, ns, name, type, id, from) { var hand = new Strophe.Handler(handler, ns, name, type, id, from); this.addHandlers.push(hand); return hand; }, /** Function: deleteHandler * Delete a stanza handler for a connection. * * This function removes a stanza handler from the connection. The * handRef parameter is *not* the function passed to addHandler(), * but is the reference returned from addHandler(). * * Parameters: * (Strophe.Handler) handRef - The handler reference. */ deleteHandler: function (handRef) { // this must be done in the Idle loop so that we don't change // the handlers during iteration this.removeHandlers.push(handRef); }, /** Function: disconnect * Start the graceful disconnection process. * * This function starts the disconnection process. This process starts * by sending unavailable presence and sending BOSH body of type * terminate. A timeout handler makes sure that disconnection happens * even if the BOSH server does not respond. * * The user supplied connection callback will be notified of the * progress as this process happens. */ disconnect: function () { Strophe.info("disconnect was called"); if (this.connected) { // setup timeout handler this._disconnectTimeout = this._addSysTimedHandler( 3000, this._onDisconnectTimeout.bind(this)); this._sendTerminate(); } }, /** PrivateFunction: _buildBody * _Private_ helper function to generate the <body/> wrapper for BOSH. * * Returns: * A Strophe.Builder with a <body/> element. */ _buildBody: function () { var bodyWrap = $build('body', { rid: this.rid++, xmlns: Strophe.NS.HTTPBIND }); if (this.sid !== null) { bodyWrap.attrs({sid: this.sid}); } return bodyWrap; }, /** PrivateFunction: _removeRequest * _Private_ function to remove a request from the queue. * * Parameters: * (Strophe.Request) req - The request to remove. */ _removeRequest: function (req) { Strophe.debug("removing request"); var i; for (i = this._requests.length - 1; i >= 0; i--) { if (req == this._requests[i]) { this._requests.splice(i, 1); } } // set the onreadystatechange handler to a null function so // that we don't get any misfires req.xhr.onreadystatechange = function () {}; this._throttledRequestHandler(); }, /** PrivateFunction: _restartRequest * _Private_ function to restart a request that is presumed dead. * * Parameters: * (Integer) i - The index of the request in the queue. */ _restartRequest: function (i) { var req = this._requests[i]; if (req.dead === null) { req.dead = new Date(); } this._processRequest(i); }, /** PrivateFunction: _processRequest * _Private_ function to process a request in the queue. * * This function takes requests off the queue and sends them and * restarts dead requests. * * Parameters: * (Integer) i - The index of the request in the queue. */ _processRequest: function (i) { var req = this._requests[i]; var reqStatus = -1; try { if (req.xhr.readyState == 4) { reqStatus = req.xhr.status; } } catch (e) { Strophe.error("caught an error in _requests[" + i + "], reqStatus: " + reqStatus); } if (typeof(reqStatus) == "undefined") { reqStatus = -1; } var now = new Date(); var time_elapsed = req.age(); var primaryTimeout = (!isNaN(time_elapsed) && time_elapsed > Strophe.TIMEOUT); var secondaryTimeout = (req.dead !== null && req.timeDead() > Strophe.SECONDARY_TIMEOUT); var requestCompletedWithServerError = (req.xhr.readyState == 4 && (reqStatus < 1 || reqStatus >= 500)); var oldreq; if (primaryTimeout || secondaryTimeout || requestCompletedWithServerError) { if (secondaryTimeout) { Strophe.error("Request " + this._requests[i].id + " timed out (secondary), restarting"); } req.abort = true; req.xhr.abort(); oldreq = req; this._requests[i] = new Strophe.Request(req.data, req.origFunc, req.rid, req.sends); req = this._requests[i]; } if (req.xhr.readyState === 0) { Strophe.debug("request id " + req.id + "." + req.sends + " posting"); req.date = new Date(); try { req.xhr.open("POST", this.service, true); } catch (e) { Strophe.error("XHR open failed."); if (!this.connected) this.connect_callback(Strophe.Status.CONNFAIL, "bad-service"); this.disconnect(); return; } // Fires the XHR request -- may be invoked immediately // or on a gradually expanding retry window for reconnects var sendFunc = function () { req.xhr.send(req.data); }; // Implement progressive backoff for reconnects -- // First retry (send == 1) should also be instantaneous if (req.sends > 1) { // Using a cube of the retry number creats a nicely // expanding retry window var backoff = Math.pow(req.sends, 3) * 1000; setTimeout(sendFunc, backoff); } else { sendFunc(); } req.sends++; this.rawOutput(req.data); } else { Strophe.debug("_throttledRequestHandler: " + (i === 0 ? "first" : "second") + " request has readyState of " + req.xhr.readyState); } }, /** PrivateFunction: _throttledRequestHandler * _Private_ function to throttle requests to the connection window. * * This function makes sure we don't send requests so fast that the * request ids overflow the connection window in the case that one * request died. */ _throttledRequestHandler: function () { if (!this._requests) { Strophe.debug("_throttledRequestHandler called with " + "undefined requests"); } else { Strophe.debug("_throttledRequestHandler called with " + this._requests.length + " requests"); } if (!this._requests || this._requests.length === 0) { return; } if (this._requests.length > 0) { this._processRequest(0); } if (this._requests.length > 1 && Math.abs(this._requests[0].rid - this._requests[1].rid) < this.window - 1) { this._processRequest(1); } }, /** PrivateFunction: _onRequestStateChange * _Private_ handler for Strophe.Request state changes. * * This function is called when the XMLHttpRequest readyState changes. * It contains a lot of error handling logic for the many ways that * requests can fail, and calls the request callback when requests * succeed. * * Parameters: * (Function) func - The handler for the request. * (Strophe.Request) req - The request that is changing readyState. */ _onRequestStateChange: function (func, req) { Strophe.debug("request id " + req.id + "." + req.sends + " state changed to " + req.xhr.readyState); if (req.abort) { req.abort = false; return; } // request complete var reqStatus; if (req.xhr.readyState == 4) { reqStatus = 0; try { reqStatus = req.xhr.status; } catch (e) { // ignore errors from undefined status attribute. works // around a browser bug } if (typeof(reqStatus) == "undefined") { reqStatus = 0; } if (this.disconnecting) { if (reqStatus >= 400) { this._hitError(reqStatus); return; } } var reqIs0 = (this._requests[0] == req); var reqIs1 = (this._requests[1] == req); if ((reqStatus > 0 && reqStatus < 500) || req.sends > 5) { // remove from internal queue this._removeRequest(req); Strophe.debug("request id " + req.id + " should now be removed"); } // request succeeded if (reqStatus == 200) { // if request 1 finished, or request 0 finished and request // 1 is over Strophe.SECONDARY_TIMEOUT seconds old, we need to // restart the other - both will be in the first spot, as the // completed request has been removed from the queue already if (reqIs1 || (reqIs0 && this._requests.length > 0 && this._requests[0].age() > Strophe.SECONDARY_TIMEOUT)) { this._restartRequest(0); } // call handler Strophe.debug("request id " + req.id + "." + req.sends + " got 200"); func(req); this.errors = 0; } else { Strophe.error("request id " + req.id + "." + req.sends + " error " + reqStatus + " happened"); if (reqStatus === 0 || (reqStatus >= 400 && reqStatus < 600) || reqStatus >= 12000) { this._hitError(reqStatus); if (reqStatus >= 400 && reqStatus < 500) { this.connect_callback(Strophe.Status.DISCONNECTING, null); this._doDisconnect(); } } } if (!((reqStatus > 0 && reqStatus < 10000) || req.sends > 5)) { this._throttledRequestHandler(); } } }, /** PrivateFunction: _hitError * _Private_ function to handle the error count. * * Requests are resent automatically until their error count reaches * 5. Each time an error is encountered, this function is called to * increment the count and disconnect if the count is too high. * * Parameters: * (Integer) reqStatus - The request status. */ _hitError: function (reqStatus) { this.errors++; Strophe.warn("request errored, status: " + reqStatus + ", number of errors: " + this.errors); if (this.errors > 4) { this._onDisconnectTimeout(); } }, /** PrivateFunction: _doDisconnect * _Private_ function to disconnect. * * This is the last piece of the disconnection logic. This resets the * connection and alerts the user's connection callback. */ _doDisconnect: function () { Strophe.info("_doDisconnect was called"); this.authenticated = false; this.disconnecting = false; this.sid = null; this.streamId = null; this.rid = Math.floor(Math.random() * 4294967295); // tell the parent we disconnected if (this.connected) { this.connect_callback(Strophe.Status.DISCONNECTED, null); this.connected = false; } // delete handlers this.handlers = []; this.timedHandlers = []; this.removeTimeds = []; this.removeHandlers = []; this.addTimeds = []; this.addHandlers = []; }, /** PrivateFunction: _dataRecv * _Private_ handler
08-08
提供了基于BP(Back Propagation)神经网络结合PID(比例-积分-微分)控制策略的Simulink仿真模型。该模型旨在实现对杨艺所著论文《基于S函数的BP神经网络PID控制器及Simulink仿真》中的理论进行实践验证。在Matlab 2016b环境下开发,经过测试,确保能够正常运行,适合学习和研究神经网络在控制系统中的应用。 特点 集成BP神经网络:模型中集成了BP神经网络用于提升PID控制器的性能,使之能更好地适应复杂控制环境。 PID控制优化:利用神经网络的自学习能力,对传统的PID控制算法进行了智能调整,提高控制精度和稳定性。 S函数应用:展示了如何在Simulink中通过S函数嵌入MATLAB代码,实现BP神经网络的定制化逻辑。 兼容性说明:虽然开发于Matlab 2016b,但理论上兼容后续版本,可能会需要调整少量配置以适配不同版本的Matlab。 使用指南 环境要求:确保你的电脑上安装有Matlab 2016b或更高版本。 模型加载: 下载本仓库到本地。 在Matlab中打开.slx文件。 运行仿真: 调整模型参数前,请先熟悉各模块功能和输入输出设置。 运行整个模型,观察控制效果。 参数调整: 用户可以自由调节神经网络的层数、节点数以及PID控制器的参数,探索不同的控制性能。 学习和修改: 通过阅读模型中的注释和查阅相关文献,加深对BP神经网络与PID控制结合的理解。 如需修改S函数内的MATLAB代码,建议有一定的MATLAB编程基础。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值