字幕列表 影片播放 列印英文字幕 Tonight is Act III: Function the Ultimate. We're going to be talking about functions tonight. Functions are the very best part of JavaScript. It's where most of the power is, it's where the beauty is. Like everything else in JavaScript, they're not quite right, but you can work around that, and there's a lot of good stuff here. Tonight, unlike the previous two nights, I'm going to be showing you quite a lot of code. Because we're talking about functions, you need to see how they work. I personally tend to fall asleep in presentations that put a lot of code on the screen; it's just kind of not a good time, so I have a lot of examples and I tried to make them all fit on one screen in big type. They're all going to be simple, but they should be interesting and useful. Let's begin. Function is the key idea in JavaScript. It's what makes it so good and so powerful. In other languages you've got lots of things: you've got methods, classes, constructors, modules, and more. In JavaScript there's just function, and function does all of those things and more. That's not a deficiency, that's actually a wonderful thing — having one thing that can do a lot, and can do it brilliantly, at scale, that's what functions do in this language. Here's what a function is. A function is the word 'function'. It optionally has a name, which can be used to allow it to call itself. It can have a set of parameters, which are wrapped in parens, containing zero or more names which are separated by commas. It can have a body which is wrapped in curly braces, containing zero or more statements. A function expression like that produces an instance of a function object. Function objects in this language are first class, which means that they can be passed as an argument to another function, they may be returned as a return value from a function, they can be assigned to a variable, and they can be stored in an object or an array. Anything you can do with any other kind of value in this language, you can do with a function. A function expression is like an object literal in that it produces a value, except in this case it produces something that inherits from Function.prototype. It may seem kind of strange that a function can inherit methods from something else, but it can. So in this language, functions have methods. That may sound odd, but we've got that. I'll show you some examples of that. We have a var statement which allows us to declare and initialize variables within a function. Because JavaScript is not a strongly typed language you don't specify types in the var statement, you just give a name for the variable. Any variable can contain any value that's expressible in the language. A variable that's declared anywhere within a function is visible everywhere within the function; we don't respect block scope. The way var statements work is the var statement gets split into two pieces. The declaration part gets hoisted to the top of the function and is initialized with undefined. Back at the place where the original var statement was, it gets turned into an assignment statement so that the var gets removed. Here we have an example. I've got myVar = 0 and myOtherVar. What that does is, at the top of the function it defines myVar and myOtherVar and sets them both to undefined. Then at the point in the function where the original var statement was, we have an assignment statement. The separation and the hoisting operation changes the way you might think of the scoping of variable names. We also have a function statement. Unfortunately, the function statement looks exactly like a function expression. The only difference is that the name, instead of being optional, is now mandatory. But in all other respects it looks exactly the same, and it is confusing to have both. Why do we have both? Well, the function statement was the older thing, and the function expression, which is really the more useful form, was added to the language later. What the function statement does is it expands into a var statement which creates a variable and assigns a function value to it. That expansion, because it's actually a var statement, splits into two things. Except unlike the ordinary var statement that we saw earlier, both pieces of it are hoisted to the top of the function, so things are not necessarily declared in the order that you think they are. It's confusing having both function expressions and function statements, so how do you know which is which when you're looking at it? The rule is, if the first token of a statement is function, then it's a function statement. Otherwise, it's a function expression. Generally, function expressions are easier to reason about. For example, you can't put a function statement inside of an if statement because of the hoisting stuff. You might want to have a different function being defined if you take the else branch or the then branch, but hoisting doesn't look at branching, and it happens before we know the result of the if, so the language definition says that you can't do that. It turns out every browser lets you do that anyway, but because the language definition doesn't tell you what it's supposed to do, they all do something different. That's one of those edge cases that you want to stay away from. In this language we have function scope. In most other languages that have C syntax we have block scope, but because of the way vars get hoisted, block scope doesn't work in this language. In JavaScript, blocks do not have scope. Scope means that, in another language such as Java, if you declare a variable inside of curly braces, it's visible only inside of the curly braces and not outside. But that doesn't happen in JavaScript because of hoisting. The variable declaration gets pulled out of the if statement and moved to the top of the function, so the variables will be visible everywhere within the function. Only functions, in this language, have scope. If you declare a variable in a function, that variable is not visible outside of the function, but it's still visible everywhere within the function. If you're coming from other languages, this can be confusing. For example, a function like this will work in most other languages and will fail in JavaScript without an error. What you'll find is that it will run forever, and that's because the programmer thinks he's created two i variables, but in fact there's only one i variable. So the inner loop is constantly resetting the i value so that the outer loop will never finish. That's something to be aware of: in JavaScript, you can't be depending on block scope. Because of hoisting, because of the way that variable statements and function statements work, I recommend that you declare all variables at the top of the function and declare all functions before you call them. In other languages the prevailing style is to declare variables near the site of their first use, and in languages which have block scope that's good advice, but I don't recommend it in this language. We have a return statement. A return statement allows a function to return early, and also indicates what value the function should be returning. There are two forms of it: there's one that takes an expression, and one that does not. If there's no expression, then the value that gets returned is undefined. It turns out, every function in JavaScript returns a value, and if you don't explicitly say what the value is, it will return the undefined value. Unless it was called as a constructor, in which case it will return the new object that you're constructing. One other note: you cannot put a line break between the word return and the expression. Semi-colon insertion will go in and turn it into a statement that returns undefined, which is tragically awful. There are two pseudo parameters that every function can receive. One is called arguments, and the other has the unfortunate name of this. Let's look at arguments first. When a function is invoked, in addition to the parameters that it declares, it also gets a special parameter called arguments. It contains all of the arguments that were actually specified in the invocation. It is an array-like object, but it is not an array, which is unfortunate. I'll show you some examples of why that's unfortunate. It's array-like in that it has a length property, so you can ask arguments how many arguments were actually passed to this function, which might be different than the number of parameters that you specify. It also has very weird interaction with parameters. If you change one of the elements of the arguments array, you may change one of the parameters that it's associated with. If you do something really scary like splicing on the arguments array, you may scramble and reassign all of your parameters. Generally, you don't want to mess with the arguments array. While the language doesn't require you to treat it as a read-only structure, I highly recommend that you treat it as a read-only structure. OK, let's look at an example. I want to have a function in which I can pass it some number of numbers and it will then add them all and return the result. The way I do that is I first look at arguments.length to find out how many numbers I'm going to be adding. Then I will have a loop which will go through each of those members of the arguments' pseudo array and figure out the total, and then when it's done it returns the total. This is how you would write that in ES3, or in the third edition of the ECMAScript Standard. This gets a little bit nicer in the fifth edition. In the fifth edition, arguments is more array-like than before. It's more array-like in that it actually inherits, now, from array.prototype, and array.prototype now contains some interesting functions like reduce. I can call arguments.reduce and pass it a function that does adding, and the result of that will be to add up all the members of that array and return it. I think it's a more elegant way of expressing the same program. Then we have the this parameter. I'm discovering that I don't like the name 'this' because it makes it really difficult to talk about it. My first sentence: 'the this parameter…' Already you're in trouble. I mean, it's just hard to talk about it in doing code reviews: 'oh, I see your problem, this is wrong.' [laughter] Well, you might be right. So what is this? The this parameter contains a reference to the object of invocation. This allows a method to know what object it is concerned with. It allows a single instance of a function object to serve as many functions. You can take a single function object and store it in lots of different objects, or put it in lots of prototypes, and allow it to be inherited by even more objects. There's just one instance of the function in the system, but all of those objects think that they have that method, and they will do the right thing with it because they use this to figure out what object they should actually be manipulating. So this is the key to prototypal inheritance. Prototypal inheritance works in this language because of this. We have the parens suffix operator, which is used for invoking, or calling, or executing the function. It surrounds zero or more comma separated expressions which will become the arguments of the function, and those arguments will be bound to the parameters of the function. If a function is called with too many arguments, the extra arguments are ignored. You don't get an error for that, they're just ignored. But they'll still go into the arguments array, so if you want to find out about them they're still accessible to you. If a function is called with too few arguments, that's not an error either. It will fill in undefined for any things that you did not include. There's no implicit type checking at all, so if the types of the parameters are important to you then you need to check them yourself within your function. There are four ways to call a function. There's the function form, the method form, the constructor form, and the apply form. They differ in what they do with this. In the method form, we have an object, and then we say dot function name or subscript, some method name, and then pass them arguments that will call the function and it will associate this with whatever that object was. That will allow the function, then, to manipulate this. Then there's the function form, in which we simply take a function value and call it immediately. In this case there's no object to associate this to, so in ES3 this was set to the global object, which was just awful. In ES5/Strict we improve that a little bit: we now bind this to undefined, which is less awful. But one problem with this form is that sometimes if you have an inner function inside of an outer method, and that method wants the inner function to have access to this, but it doesn't have access to it because it has its own this which is different than the outer this. So in order to make this visible to the inner function, the outer function can declare a variable, perhaps called that, assign this to it, and then the inner function will have access to that. We have a constructor form, which looks like the function form except we have the new prefix. Now when the function is called, this is bound to a new object that inherits from the function's prototype number. Then if the function does not explicitly return a value, that new object will be returned. This is used very much in the pseudo classical style which we'll look at a little bit later. Then finally there's the apply form in which we use either the function's apply method or its call method. What they have in common is they both allow us to specify what this is. The value that this should have will be the first parameter. The difference between them is that apply takes an array of arguments and call takes zero or more individual parameters, which will become the arguments. I showed how to define call in terms of apply, and also show a little bit of the ugliness that's caused by the fact that arguments is not a real array. What I want to do to implement a call is I want to take all of the parameters that were passed except for the first one, and I do that by using the splice method — except arguments doesn't have a splice method in ES3, so instead I have to go out and find it. I know that I can find it at array.prototype, so I go array.prototype.slice.apply, and then I can take that piece of arguments. Really awful. Again, we fix that in ES5 a little bit. To summarize, this is a bonus parameter, and its value depends on the calling form. If its call is a function, it's bound to either the global object in ES3, or to undefined in ES5/Strict. If it's called as a method it's bound to the object containing the method. If it's called as a constructor, it's bound to the new object being constructed. And if it's called in the apply form, then we explicitly pass in an argument that determines what this is going to be. We call these things functions, but they don't behave exactly like mathematical functions. In a mathematical function you would expect that every time you use a function with a particular set of inputs, you should get exactly the same outputs. There are some programming languages in which people are trying to match that ideal and there is some attractiveness in doing that, because the behavior programs write is more predictable, it's easier to reason about them, and they're also a lot harder to write. Because it turns out that programs, in order to be interesting, are interacting with the world, and the world is always different. The functions are always going to be dealing with different things, so they'll tend to want to keep state, and to manipulate that state and to mutate things. So functions will tend to have side effects. In JavaScript you can program in the pure functional sense, in that you can assume: OK, I'm never going to assign to a variable, and I'm never going to change any object once it's created. And the language will let you do that, but you're going to find it's really hard. We tend to change things a lot, because it's just an easier style of programming. Where did functions come from? Originally, there was something called the subroutine. The subroutine began life back in the assembly language era, where you'd want to be able to define your own op codes, and you could take a bunch of instructions that you used frequently and create a pseudo op and call it. Subroutines were born. They introduced the idea of call and return, where we call the thing and when it's finished it comes back and we resume from where we were. That idea has been in virtually every language since then. In different languages they've been called subs, procedures, procs, funcs, functions, lambdas, but it's all the same idea of taking some specification of computation and packaging it so that it can be re-used conveniently. The first motivation for sub-routines was code reuse. The first generations of computers had really small memories, so in order to get programs to fit you'd want to take pieces of the program that were recurring and factor them out so that they were only there once, and then call them. That was the only way you could hope to get it to fit. It turned out that was such a good idea that was then used in the design of programs. Treating a program as a single, monolithic list of instructions was too difficult to reason about, so if we'd divide and conquer that program into smaller components then we can think about those components more easily. A subroutine or function was a natural form for doing that. The next step was using them to do modular things — for example, to create libraries of routines that could be loaded with any program so that you could have stuff that could be reused from one program to another. That led to a sense of expressiveness where, in thinking about how to design an application, you would first think of what set of subroutines would make it really easy to write this application, essentially designing a programming language expressed as subroutine calls, which are ideal for implementing this application, and then write those subroutines. The next step up was higher order functions, in which we're going to do things with functions which couldn't be done otherwise. That's when some of the power of the language really starts to work for you. One of the cases where that occurs is in recursion. Recursion is when a function calls itself, or is defined in terms of itself. Now, at first this didn't make sense to some programmers. For example, if you were working in FORTRAN, FORTRAN couldn't do this. It was not possible for a function to call itself in FORTRAN. A lot of very good programmers looked at the idea of recursion, and reasoned: well, I've never used it, and I don't understand the need for it, therefore it could not be very important. It turns out it's actually really important, and when you learn to think recursively you become a much stronger programmer. One of the classic algorithms for a recursive solution is the Quicksort, which was invented basically because ALGOL was invented. Expressing this in a recursive programming language turned out to be really easy. There are basically two steps in Quicksort. The first is you divide an array into two groups: all the big values and all the small values. One way you could do that is you could have two pointers that are going through the array, and one starts on the small end and one starts on the big end, and when either finds something that's in the wrong group they swap them, and then continue scanning in until they meet. When they meet, you're done. Then you go to step two, where you take each of those groups and call Quicksort on those groups, and you're done. That's the whole sort, and it's really fast. There are more optimizations you can do to it that make it even faster, but just doing what I was describing in the average case is n log n, which is really good for a sort, and you hardly do anything. It's just really, really simple. So once you can learn to think recursively, a lot of really interesting things fall out. Here's another kind of recursion. You might recognize these from the JSON language. We've got the syntax diagrams for values and arrays, and you might notice that there's a dependence issue going on here where a value can be an array, but an array can contain a value. A naïve programmer might struggle, thinking how do I organize my functions in order to parse something like this? I've got this circular dependency and that's really hard, but it turns out if you have recursion working for you, there's a trivial solution to this. Here I have two functions. Each exactly implements one of those syntax diagrams. I've got the value which, when it sees a square bracket, will call the array method and array function, and return whatever it returns. Then I've got my array function, and for each of the things that it finds it calls value to find out what it is. Here I've got mutual recursion going on, and it all works out. You don't have to think about how to manage the transition from one to another, just the ordinary function plumbing does all of that work for you. You don't even have to think about it. Lisp had this stuff going on in 1958, ALGOL had it in '60, but it took awhile to get into the mainstream languages, partly because people couldn't think about how to implement it efficiently. That time, the way subroutine calls worked, on most machines you had self modifying instructions, so when you called a function it would destroy whatever is in the first word of the function and replace it with a jump back to the place that it was called from, and you couldn't do that recursively because once you've clobbered that address there's no getting back. So you needed some other place to keep the return values, and eventually that turned out to be a stack. All modern CPUs now have support for that, usually in the form of auto incrementing or auto decrementing pointer instructions. These assembly language notions eventually found their way into programming languages, so these things come straight out of assembly language but found their way into C and then into Java, and everything else. I don't like them. They look way too primitive and fish brained to me, if you know what I mean. I think we can do better than that. One of the other key ideas which was, again, alien to people who'd never used it, was closure. People who were working in languages in which closure was not an option were like 'I've been programming for years without it, I don't understand why you'd ever want it.' But it turns out JavaScript's got it, and it's really, really good. That's where we're going to spending most of out time tonight. It's sometimes called lexical scoping, sometimes called static scoping. It has to do with how variable names are resolved in nested functions. The context of an inner function includes the scope of the outer functions, so all of the variables that are in the outer function are available to the inner function, and this continues even after the parent function has returned. That sounds kind of weird, so I've got a lot of examples to show you what this means. I'll start with a simple one. I've got a function called digit_name, and digit_name will take a number as an argument, and will return the name of that number in English. It will take advantage of an array of strings it stored in names. As you can see, it's a really simple function. Unfortunately, the way I've defined it here, names is a global variable. The problem with that is, if there's anything else in the environment that is also a global variable that has that name, they're going to interfere with each other and will probably cause this to fail. That's something you cannot test for, because it's impossible to test with everything that might be loaded on a page. For example, it might be that a third party ad gets loaded one day that happens to have a global variable called name, and now your page died. That's intolerable, so we want to, as much as possible, reduce our dependence on global variables. One way we could do that is to rewrite this program so that names is now a local variable of the digit_name function. And that works. It's a local variable, we have function scope, names is not visible on the outside, so even if an evil ad comes in and has a names variable it will not interfere with this one, so that's good. This is a much more reliable version of the function. Unfortunately, every time we call the function, we're going to allocate a new array and stuff ten things into it, which is going to take some time. We don't want to do that; that's a terrible waste. In this case it's a fairly trivial thing, but we might have a more complicated function with a more complicated initialization, so we want to be able to factor that out. Closure provides a really nice way to do that. Now I have a function and it has a private names variable, and it returns a function. The function it returns is assigned to digit_name. The important thing is, notice at the bottom, we're invoking the function now. We're invoking the function immediately, so what I'm storing in digit_name is not the whole function, it is the function that it returns. OK? This is really important. In order to give the reader a clue that there's something interesting going on here — because assigning a function looks almost the same as assigning a function that's immediately invoked — I wrapped it in parens. The whole thing is wrapped in the golden parens. That's a clue to the reader; it's not required by the language, but I think it is required by humans. It gives us a clue that there's something really interesting going on here. We assign the return value of the outer function to digit_name. The outer function has now returned, digit_name now contains a function, which is the green function. That green function still has access to names, even though names is a private variable of a function that's already returned. That's closure: one function closes over the variables of another function. This turns out to be one of the most important features in JavaScript; this is the thing that it got amazingly right. This is the thing that makes JavaScript one of the world's brilliant programming languages. There's another pattern going around called lazy function definition. I show you think as a warning. Don't do this. The idea here is that, in this form, I unconditionally initialize the function before we're going to start calling it. But what if the initialization is really expensive, so we don't want to do it unless we know the function is going to end up getting called at least once? This lazy pattern attempts to do that. What it does is it assigns to digit_name a function, and when that function is called it will then store another function into the same variable. So it'll replace itself, it'll modify itself. The idea here is that that allows us to avoid having to initialize the thing, if we don't need to do it. But it comes at a cost, and the cost is confusion. Digit_name is no longer first class in that if I were to pass it to a function and let that function call it, or if I were to assign it to an object and let someone call it as a method, every time it gets called from that point on it will do the initialization and stuff a new function into digit_name. Instead of making it faster we've actually made it slower. It's slower than the slow case we started off with. Now, the counter-argument is, OK, you've got to be really careful to not do that, so one of the rules we'll put in the documentation is that this can only be called from the global variable, you can't use the function value as a function value except to call it immediately, and that it's worth it because we're saving the initialization cost. It turns out that analysis is wrong. All we're saving is the cost of an if per iteration, and let me show you why that's the case. Here we're going back to the closure form, except I put an if statement in it, so that if names hasn't been initialized yet, we'll initialize it now, and then we'll do what we always do. The cost of this compared to the previous one was one if statement per invocation, which is in the noise, it's not even measurable. The optimization that we were hoping to get in the lazy form just doesn't pay off, and we get weirdness instead. Now, an argument about that might be: well, suppose we call this function a million times, or a gazillion times. A gazillion if statements, that starts to add up to something. You can go yeah, maybe that's true. But if you think you're really going to call this a gazillion times, we shouldn't be optimizing the case where we're not going to call it at all. [laughter] I thought I heard some applause there. Maybe not. [laughter] OK, here's another example. A fade function. This is something you might do in an Ajax application. I want to take some object — maybe a div or something — and have it fade from yellow to white, maybe as an indication to the user that something changed and they should pay attention to it. I've got my fade function. First thing I do is find a DOM element and create a variable called level, which I'll set initially to 1. Then I'll define a step function, and then I will call setTimeOut, passing that step function with a time, so it'll fire in a tenth of a second. And then it returns. Done. That's the end of fade. Then suddenly, a tenth of a second later approximately, the step function executes. It will first define a variable H, and initialize it with level. What is level? Level is the variable of fade. It's not the value of fade when it was created, it is the current value, it is the current variable. It does the same thing with DOM — it gets access to the DOM variable and uses that to change the background color of that DOM node. It then looks at level, and if it's less than 15 — which it will be, at this point — it will add 1 to it. It's adding 1 to the level variable of the fade function that's already returned, and then it will call setTimeOut, and in a tenth of a second will do this again. It will keep doing it until eventually we reach 15, and then we stop. Now, suppose we had three things on the page and we wanted them all to fade simultaneously. We call fade 1, 2, 3, with three different IDs at the same time — are those three executions going to interfere with each other? No, not at all. Because each invocation of fade has its own unique set of variables: its own DOM, its own level, creates its own step functions, and they do not interfere with each other at all. So this works, again, because of closure. Because step is able to close over the DOM and level variables, it just works. Everybody still with me? OK, one more example along these lines. I want to make a later method. It's like setTimeOut except more object oriented, so I want it to be a method of all objects. I can take for any object, call later, give it the number of milliseconds in which to wait. It doesn't actually wait, it puts it on timer queue, and eventually it'll get around to dispatching it. Give it the name of a method, or perhaps pass in a function which will be treated as a method, and then the other parameters of that method would need. On the next screen I'll show you what it looks like. But again, I'll point out the problem with arguments. What I'm going to want to be able to say is: arguments.slice(2), so that I can take all of the parameters that were passed except for the first two and make a nice little array out of it. I can't do that in ES3, instead I have to write array.prototype.slice.apply(arguments, [2]), which is pretty nasty. So when you see that on the next screen, you'll know why that is. In ES5, you can do the simpler thing. I'm going to add this to object.prototype. I could add it to any of the ancestors of my application. This is one place to put it. Object.prototype is a global object and all of the problems you have with global variables you have with global prototypes as well, so this is something you want to do really cautiously. You want to do it conditionally, just in case the language ever actually adds later as standard equipment, so that you're not going to be replacing the official version with your version. Generally you don't want to be doing this in applications, although it's sometimes a reasonable thing to be doing in Ajax libraries. In this case, if we don't already have an object.prototype.later method, we're going to define one. We're going to pass in the number of milliseconds in the method, and then we'll create an array of the additional arguments. We're binding that to this; it's doing the thing I showed you before, because in the green function we're going to want access to this, but this doesn't work. This is not captured in closure. But that is, and so that's how we get that into it. That will call setTimeOut, and will cause that function's method to get invoked at that time. One other thing I'm doing here is when later is finished, which happens immediately, it returns the value of that, which is also this. The advantage of doing that is it allows us to then cascade on that. So if I had several things that I wanted to have happen later but at different times, I could say myObject.later5.later10.later20, and so on. I could just cascade all these things one after another because each returns its own object, so we can then go right on and invoke the next one. There are a lot of Ajax libraries that carry this idea to excess, but it's a really nice pattern, and I think it works really nicely in this language. Another example: partial application. We're starting to get a little theoretical now. Partial application says I'll take a function and a parameter and return another function which doesn't execute that yet, but will when it's supplied with additional parameters. Let's start with the example first. Using a function called curry, I'm going to pass it an add function — which takes two arguments and adds them together — and I'm going to pass it 1. It will return a function which will add 1 to whatever gets passed to it. I'm going to store it in increment, because that's a good name for that, and then I can call it. So if I now pass a 6 to inc, I get 7. This is called partial application. The implementation of it is, I'll first get an array of arguments, except for the first one, because the first one is the function and I don't need that one. In this case I'm assuming I'm on ES5, so I'm not doing the awful array.prototype.apply trick. Then curry returns a function, and that function will apply the arguments to the function. One bit of weirdness that's left over from arguments not being a real array is that if I pass arguments as a parameter to concat, it doesn't recognize that it's an array and then take all the members of it and concatenate them to the other thing. It will concatenate them as a single array, which is not what we want, in this case. We need to turn it into a real array so that concat will do the right thing to it, and we do that by calling its slice method. ES5 has the slice method, so slice returns an array, and that will work. But we shouldn't have had to do that; there's still some things left to get fixed in future editions. Everybody still with me? OK, here's one other. Suppose we've got a process which cannot be resolved immediately. Maybe it's going to require a lot of computation, maybe it has to go out to a worker pool and do something, maybe it has to go back to the server and get some stuff. But we'd like to be able to return something immediately that we can start acting on, even though it's not going to be real for awhile; we don't know when that while is yet. A service that's doing something like that could return something that's called a promise, and the promise is an object which allows us to call methods on the thing. If we know what the thing is then it will immediately get executed. But if we don't know what the thing is yet, it'll get cued up. It will finally get executed when we know what the thing is. That turns out to be a really useful pattern for doing a lot of things, particularly when you're doing a lot of communications. Here we're going to implement a promise maker, and the promise maker will return a set of five functions: when, fail, fulfill, smash, and status. You could pass any one, or any fraction of these functions to someone else. For example, you might have a service, and I want to return something to you immediately. I give you back an object containing a when and a fail method. You can then pass to when functions that you want called when the thing is fulfilled. You can also pass functions to fail for the case where a failure comes back. It'll just sit on all those things until it knows what the disposition is. And then the creator of the service might hang on to the fulfill and smash methods. Fulfill he'll call and pass a value in when he knows what the value finally is, and that's the thing that will get delivered to the functions. If it turns out that it's going to be an error, at this point it turns out it's too late to throw an exception because that was a long time ago, and the other guy's not in your call stack anymore, so instead you smash the promise, you break the promise, and that will cause all of his fail methods, now, to run. The way these things work is they depend on the vouch and resolve methods, which are private to the promise maker. But again, it closes over, so it'll always have access to those functions and the state that they refer to. Let me show you implementations of vouch and resolve. First we've got a few more variables. We've got status, which initially is unresolved, and eventually could be fulfilled or failed. We've got the outcome, so when we know what the value is we'll stick it in there. We've got the waiting list of functions that were registered with when. And we've got the dreading list for the functions that were registered with fail. Then vouch will take a deed and a function and then it'll look at the status. If the status is still unresolved, then it will put it onto one of those lists. Which list it will put it on will depend on what the deed is. But if the current state of the promise matches the deed, then we can execute it immediately. Then the other piece of this is resolve. If the status has already been resolved then we throw an error, because we can only do it once. Otherwise, we'll go through and use one of the nice thing in ES5 now: we've got a forEach method. We'll figure out which of those two arrays of a function we've got, and we'll say for each one of those functions, 'call this function'. This function will then go and call each of those with the value. We had to wrap it in a try catch, because if any of those functions should throw, we don't want that to interfere with the other functions getting a chance to run. OK, everybody still with me? We'll look at one more: sealers and unsealers. Sometimes we'd like to be able to pass secret information around through the application. Say that I give to you a secret envelope and tell you to give it to the cashier, and the cashier will take care of you. I want you to be able to take that envelope to the cashier and get reimbursed, and I'd like you to be able to give that envelope to someone else and allow them to be reimbursed. But I don't want you to be able to open it yourself, I don't want you to be able to tamper with it, and I want the cashier to be able to verify that it is, in fact, the original un-tampered-with thing. We can do that really easily in JavaScript, it turns out. It sounds like something you'd need cryptography to be able to do, but that doesn't really work inside of an application. But it turns out there is a much simpler solution. The way is works is I've got a sealer maker which will return a pair of functions, a sealer and unsealer, and they have to be used in the pairs. I will keep the sealer, and I will give the unsealer to the cashier, and then I can call the sealer with the value that I want to give to you, and it will return to me a box which I can then give to you. The box is useless to you, except that if you can give it to someone who's got an unsealer, they can reclaim the original object. This function is a tiny bit harder to write than it should be, because in JavaScript object keys have to be strings, they can't be objects. If they could be objects, this function would be totally trivial. As it is, it's just slightly trivial. What I will do is I'll create the box, the secret container, which is just an empty object. It's really just a token; I'm not actually giving you a real box, but it acts like a box. I'll store it in my box's array, and right next to it I will store in my value's array the value that it represents, and then return the box to you. That was really easy. Then the unsealer uses the new indexOf method that we have in arrays, and goes looking for that box in the list of boxes. If it finds it then it returns the corresponding value, and then we've got it. If something goes wrong, if you pulled a substitution, gave an object that was not sealed, you get undefined back, which is how it should be. We're going to shift slightly and start looking at inheritance, but we're still going to reflect it back onto what we can do with closure. Here's an example of how you can do things with what I call pseudoclassical inheritance. This was the inheritance scheme that was designed for the language, and I really don't care for it at all. I don't think it looks very good. Here we're defining a gizmo, and you can see the gizmo's constructor. Then we add to the gizmo's prototype the methods that we want the instances to inherit. This just looks really weird. We're sort of used to the idea of a class containing all of its stuff, and in this case it's kind of hanging on the end of it in a haphazard way. It also induces people to do things incorrectly. For example, I've seen people trying to assign functions to prototypes inside of the constructor because it just seems like that's where you should do it, and doing it on the outside just feels wrong even though that's how you're supposed to do it. It gets even worse in the case of the hoozit where I want the hoozit to inherit from the gizmo. The way I specify that in the language is I replace hoozit's prototype with a new instance of gizmo, and that just looks crazy. And it's potentially dangerous. It turns out that the gizmo constructor would throw if there were no parameters, then it would actually fail. But this is the way the language was intended to be used, and it's because the language itself is confused about its prototypal nature. I think there's a better way to do this. So let me suggest another formulation of exactly these same objects. Here I'm going to make a gizmo, and to make it for me I'm going to call my new constructor function. It will make the new instance of gizmo, or the new definer of gizmo. I will pass to it object because I want gizmo to inherit from object. I'm going to pass to it the constructor function, and I'm going to pass to it an object containing the methods that it should add to its own prototype. This does exactly the same thing that we saw on the other screen, but I think it's just more pleasant looking. Then it gets even better with the hoozit. With the hoozit I call new constructor, pass in the gizmo that says I want hoozit to inherit from gizmo, and I also pass it a constructor. I'll also pass it an object containing additional methods that I want it to add to its prototype. To my eye, this looks a whole lot more rational than that did, with all the stuff hanging out and the weird replacement. The language doesn't provide the new constructor function that you need to do this, but it turns out it's a really easy function to write. So let's write that function. Function new_constructor takes three parameters: extend, initializer, and methods. The first thing it does is it creates the prototype object, which it makes by calling object.create. Then if there are methods available it will call the keys method — this is a new thing in ES5 — which will return an array of all of the own keys of that object, which is really nice because an array has a forEach method, so it will then call that. That will allow us to easily copy all of the methods into the prototype. It's a really nice construction. Then we'll create the function itself, which we'll use to make our hoozits or whatever, and you can see that closure's working in there because it has access to prototype, and it has access to the initializer. So it will create a new instance of the prototype using object.create, which makes a new object that inherits from the object that you pass in. It will then call the initializer, passing that same object in, and when it's done it will return the object that we just created. So this does the same thing as new, except we don't use new. Then a little bit of extra plumbing — we don't really need to this, but just to be nice we'll set the function's prototype property to the prototype, because in the case of the hoozit, the prototype got replaced, so we lost the constructor value. We'll fix that there, as well. Again, we're using closure in order to implement a classical pattern, and I think this works really nicely in the language. Another thing we can do with functions is to create modules. We'd like to be able to minimize using global variables because of the conflicts that they can create, and functions provide a very nice way of doing that. Here I want to create a singleton object — there'll just be one instance of it — so you don't want to have to create a class to define something there's just going to be one instance of; that'd be silly. So I'm going to assign to singleton not that function, but the consequence of calling that function. Again, I'm wrapping the whole function and the invocation in parens as a sign to the reader that there's something bigger going on than just assignment of a function. There are some people who would put the golden paren around the function, and not around the whole invocation. That doesn't make sense to me, because what we're trying to tell the user is: look at the whole thing. Putting parentheses around just part of it is, I think, counter productive. I think the whole thing needs to be wrapped in parens. The outer function has variables and functions, and they will return an object using an object literal, and the object will contain some methods. Those methods will be closed over the private stuff. We're returning, in this case, two functions. In the earlier cases we returned one function, but this time we're returning two. We could return as many as we want. And they share their access; they're both closed over the variables of the parent function. So they can communicate through that shared state without corrupting the global space. A related pattern to this is if we want to have a common global object where we'll keep our whole application. At Yahoo! we keep a lot of stuff in a global Yahoo! object, so everything that's ours we keep in one common namespace. I want to add a new thing to my global object called methodical, which will have my two methods in it. Just as before, I'm going to be assigning the result of my function into that object. Now, sometimes I want to be adding not a new object but just a couple of methods to that structure. I can do that as well. Here's another variation on the same pattern. I've got a function, and it's got the private stuff, and then I'm going to assign to GLOBAL.firstMethod my first method, and to GLOBAL.secondMethod my second method, the other one. Again, the whole thing is wrapped in the golden parentheses. In this case, the parentheses are syntactically required, and that's because I want this to be a function expression and not a function statement. If it were a function statement, I couldn't immediately execute it, and I want to immediately execute it. Everybody still with me? I can take this module pattern and very easily turn it into a constructor pattern. It's the same basic idea, I'm just going to make lots of instances, not just one instance. Here's the recipe. Step one: make an object using any of the techniques available in the language. I can use an object literal, I can use new, I can use object.create, I can call another of these power constructors and use the thing that it returns. Then step two: I define some variables and functions, and these will be the private members of the object that I'm about to make. Step three: I augment the object with privileged methods. A privileged method is a method which has access to that private state, that closes over the private state. And step four, I return the object. Really simple recipe, but it's a little abstract, so let me turn it into a template that's a little easier to follow. Step one. This is going to be my new power constructor, and I'm going to create a variable called 'that'. I can't call it 'this', because 'this' is a reserved word. I will initialize it somehow; somehow I'll turn it into an object. Then step two, I declare secrets, the secret variable, stuff that's going to be available to my privileged method. Step three, I create my privileged methods and assign them to that. Step four, I return that. So it's really simple. Here's gizmo and hoozit again. This is how we would write it, again, in the classical style, pseudoclassical style. It so bothers me how all this stuff's hanging out. Also, gizmo's got a constructor, and hoozit's got a constructor, and they both do the same thing. So even though one inherits from the other, we don't get the advantage of that code reuse. There's some redundant waste going on there. I want to apply this functional system instead of doing this. This is how we'd write it. I've got my gizmo, it returns an object literal, done. That was really easy. Then my hoozit calls gizmo to create an instance, it augments that, adding its test method, and returns that. Done. So it's really simple. But there are some other benefits that come from writing in this style. One is that we've got privacy. Right now, with the way it's written, the ID is a global property of the object, so anybody could go in and get the ID directly or modify it. Maybe I don't want them to be able to do that, maybe the integrity of my object depends on nobody being able to mess with the ID. Writing this in the functional style, we can do that — not only can we do that, the code gets simpler. We just don't have the ID property in the object. We're referring now to the ID parameter, and because of closure, our two string method always has access to that parameter. So we just took the 'this's out, and it's done. We do a similar thing with hoozit. So again, it just became simpler. There are other things we could do, too. We could have a shared secret which we pass between all of the constructors, which could be used to simulate something like a package relationship, where they all contain something that they know. You can get arbitrarily complicated with this stuff; you usually don't need to get anywhere near this fancy, but it's nice knowing that you can, if the need should ever arise. When I started working with this language, I spent a lot of time thinking about how to simulate things that we did in the classical languages, like how do we get super functions? In the pseudoclassical model there's no easy way to write super functions, but in the functional style it's really easy. Just capture a super function from the thing that I'm inheriting from, keep that in the closure, and then I can call it at any time I want. It turns out, though, in my career with this language I've never once written a super function. I just think about things in a different way so that that style of dependency that I've come from, I just haven't found the need for it. So if you find yourself wanting to have super functions, you might step back and figure out: why do I think I need that? Maybe there's a simpler way to think about this. Here's another thing we can do. I want to have a memoizer, which will remember the result of previous callings of a function — particularly recursive functions — so that we can avoid doing some work. For example, factorial can be given a recursive definition in which it's the product of the value and of calling factorial on the value diminished by 1. If you're computing a table of factorials, you could spend a lot of time going over the same ground over and over and over again, and this function will prevent that. What I'm going to pass to the memoizer is an array containing some of the values that we're going to remember. The results for factorial of 0 and factorial of 1 will be 1 and 1, so we'll pass that in to get it started, and then we'll also pass in a function that defines what a factorial step is. In this case, it's multiplying n times the recurrence minus 1. When we go up to the memoizer it takes that memo array and it takes the formula we just passed in, and it will create a recurrence function, which is the thing that will call for each iteration, which will first look to see if we already have the result that we need in the memo array. If it does, then we're done. If not, then we will call a formula passing in itself, its own recurrence function, so that it can do the next step. Where this is a big win is in computing Fibonacci, because Fibonacci recurs on two legs at the same time, so it gets explosive. If you do a Fibonacci of 40, say, it's in the trillions of iterations, and this gets it down into the tens. So even though the program looks a little bit more complicated, it's hugely more efficient. Again, this is happening because of closure, because the recur function closes over the memo array and over the formula that we're recurring on. One bit of warning about functions: don't declare functions in a loop. Don't make functions in a loop, for two reasons. One is it can be wasteful, because a new function object is created for each iteration. It's just wasteful. JavaScript compilers tend not to do any kind of loop and variant analysis, so anything you're doing in a loop that doesn't change over each iteration, you probably want to move it out of the loop anyway just to make it go a little faster. But the bigger reason is that it gets really confusing, because you think that you're closing over the current value of the loop variables but you're actually closing over their final values, or their current values, and that's almost always not what you want. Let me show you an example of a really common error. Say you've got an array of divs and you want to attach an event handler to each one. You go through the array in a loop and for each one you want to add an onClick handler which will display its ID number when it's clicked on. What you find is that they all come up with the same number, and it's the wrong number. You wonder, how did that happen? It's because when you add the function to onClick it's closing over div ID, which is constantly changing. By the time you finally get around to clicking on them you're going to be getting the final value, which was the value that kicked you out of the loop. The way you get around that is by creating a separate function which you're going to use to assign the functions to the event handler. Here I have a function called make_handler which will take the div ID and return the event handler function. Then within the loop we call make_handler and take its result and stuff it into onClick. By doing that, we avoided creating any functions inside of the loop, and that way we avoided the confusion that came from that problem with closure. Here I have two versions of the factorial function that do exactly the same thing. The only difference is that one of them uses a variable, and the other uses a parameter to represent result. Otherwise, they're exactly the same. R. D. Tennent wrote a book called 'The Principles of Programming Languages' in which he demonstrated the Principle of Correspondence, which was a correspondence between variables and parameters. JavaScript demonstrates it really well. This shows that you could imagine a subset of JavaScript which didn't have variables — would that still be a useful language? It turns out yes, and this is the proof that anything you can write with variables you can write without variables. You can use a function closure instead to do the same thing. We can take that thought experiment one crazy step-off-the-edge farther. Suppose we had a language in which we didn't have variables and in which we didn't have assignment, and we didn't have named functions. Could we still do recursion? It turns out you can. I'm not sure you'd want to, but you can. Here is the strangest artifact in computer science: it's called the Y Combinator. It's a function. It's a really complicated function, although it's not very big. It's incredibly nested; functions within functions, calling themselves, passing themselves as parameters to themselves. I call Y passing in a factorial formula. It returns a function, and the function it returns is the recursive factorial function. This is really wild stuff. If you can figure this out, you can call yourself a computer scientist, because this is the really good stuff. You can express this stuff in JavaScript — I mean, JavaScript is right up there with Lisp and Scheme. It is a functional language. You can do this stuff. While this may have little practical value, in terms of increasing your powers as a programmer, this is the stuff to be playing with. You can get really, really deep. I see a lot of people playing with their Ajax stuff, or wanting to show off — look at all the stuff I can do — and sometimes doing things which are probably reckless and ultimately not very smart. If you want to show that you're really smart, you ought to be doing this stuff. You know, off to the side, where you're not going to hurt anybody. [laughter] JavaScript has good parts. It has really good parts. And these, I think, are the best of the parts. Again, this comes as a big surprise, because when JavaScript was introduced nobody expected there was anything good about it at all. The stuff that is good about this language is in there intentionally, by design, it wasn't accidental. You don't get stuff this good by accident. This is an amazingly good language. And that's why Ajax happened — we'll be talking a lot more about Ajax next week. The reason I was able to discover that JavaScript had good parts was because I knew something about functions. The place where I first learned about functions was in a little book called 'The Little LISper', which I highly recommend to you. The current edition of it is called 'The Little Schemer' — it was updated to be about Scheme. It's not really about Scheme; there isn't very much Scheme in the book. It's mostly about functions, and it's really, really good. It turns out that everything in the book can be written in JavaScript. Although Scheme and JavaScript couldn't be more different syntactically, at their roots they're surprisingly similar. There's a simple transformation from one language to the other; it's surprisingly simple. If you go to this web page, it'll show you exactly what they are, and that'll give you enough to be able to read and write the examples in the book. I highly, highly recommend that you go out and get this book. It will change the way you think, and there are very few books that do that. This is one of those books. Next time we meet: The Metamorphosis of Ajax. It'll be awful. [laughter] See you here. Thank you, and good night. [applause]
B1 中級 Crockford on JavaScript - Act III: Function the Ultimate(函數是終極的) (Crockford on JavaScript - Act III: Function the Ultimate) 106 12 kleeff 發佈於 2021 年 01 月 14 日 更多分享 分享 收藏 回報 影片單字