看 BBC 學英文
Russell: Good afternoon.
I'm Alex Russell.
I'm a software engineer on the Chrome Team.
Thanks for tearing yourself away from lunch
So like I said, I'm an engineer on the Chrome Team,
and before I joined Google,
I joined the Chrome Team from a web development background,
and before that,
working web application development and security.
And before my recent detour into C++,
I spent most of my day trying to figure out
I think when a lot of us started to be cognizant of the web.
that was starting to become powerful
and somewhat standardized
and somewhat widely available on the back of the browser wars.
So I want to talk a lot about
why it's so important, why that history,
that long history continues
to figure into the sorts of things we try to do
And I want to take you through what's really
that makes it so different to the languages
that you might be using in your day-to-day work
Exactly where are we right now?
Because I think this is also sort of
an ambiguous question, right? If you're a browser bender,
it's easy to say we're this fast--easy-ish.
If you're a web developer,
you can look in your deployed base
and understand who's got what,
but that doesn't necessarily tell you anything
about the future.
And I want to spend a lot of time today talking about
how it is that we are going to get a different future
is starting to move again,
and I want to tear back the veil
that covers each of these topics
and help you understand exactly what's going on
and how these parts relate to each other, because they do.
The history informs the future,
and the current state informs what's going to happen next.
So how many of you write in other functional languages
or are familiar with functional languages?
The front of this talk is going to be something
that maybe just warms your heart and may not be new information.
In the sort of the history of functional languages,
it supports closures.
Much like Scheme,
it's got this nice first class function system,
and in 1995, Brenda and I hacked it up as a little prototype
which made its way into Netscape 2,
and Netscape 2 was the first browser
to really have any serious scripting supported.
It had DOM level 0, and soon afterwards,
once it escaped into the browser,
started the standards process at ECMA,
not inside the W3C,
because the W3C wasn't the place for languages.
as a standard.
In 1996, Microsoft shipped IE 3.0,
and that was the first version of JScript.
So we now had two competing,
mostly compatible implementations
of a scripting language for the web.
Nobody knew how big the web was going to be,
although, at the time,
it looked like things were trending up.
Well, we know how that story ends.
Today, you can't ship a credible platform
that doesn't have the web.
In 1999, ECMAScript version 3
was finalized at TC39, which is the technical committee
And from its humble roots
that was released in 1999 has powered us pretty much to today.
It has been the thing that for more than a decade
has gone pretty much everywhere.
has been built in to the OS for Windows since Windows 98.
You can't get a copy of Windows,
you can't buy a copy of Windows today
and every browser on the planet now includes
and these things are moving really, really fast.
I work on the Chrome Team; therefore, I'm partial to V8.
It goes nice and quickly,
but these things are all compatible
to a very high degree.
All of these implementations are separate implementations,
and they're competing on things that aren't,
"Oh, here's a new language feature."
We're collaborating on language features
in the standards committee,
but we're competing on performance,
which is a really great place to be for a language,
because it takes a lot of investment
to make a language a real success.
It takes a lot of deployment,
a lot of competition, and a lot of investment
to sort of get the really smart guys
that it takes to go make a VM go like hell.
of this sort of ecosystem of investment in a way
that only a couple of languages before it ever have.
than they have been in the past, even the recent past.
And so every device that you get today,
including the tablets that were handed out to you yesterday
and the Chrome books that you'll be receiving as attendees
on June 15th,
because the web is the platform.
The web is the way that you're going to build
a lot of the applications
that your user is going to care about,
and it's the way to build portably today.
And I think what I want to do first here
is to talk you through the parts
that most people don't really have a sense of,
when they talk about,
Because we hear it's a dynamic language.
We hear it's a functional language.
But it looks a lot like C. It looks a lot like Java.
It looks a lot like C++.
And some folks have been doing a great job
in the last couple of years at sort of helping to tear back
the blinders that are on us,
as folks who come from a C or C++ background,
and help lay out what it is.
But I want to go through it very briefly,
because I think it's important to understand
what's actually in there,
because when we understand what's actually in there,
we'll understand how the language can evolve,
because you don't want necessarily separate--
competing ideas to be existing inside the same language.
You want a language to sort of have a theory of itself.
You want it to be coherent in ways that make it,
so that when you understand one part of the language,
you can understand the next part of it.
And my interest in this is coming from a background
as a web developer,
I serve as one of Google's representatives to TC39,
And so I have a particular and keen interest
in making sure that we evolve the language in ways
that are reasonable and solve real world problems
that we've all got.
There aren't a lot of core concepts.
There's no type system, per se.
There are types.
You can have a number, or an object, or an array,
but there's no type testing.
There's no way to define your own types, necessarily,
and have them participate at, like, function call time
and have the system throw an exception for you,
unless you do the testing yourself.
That means that if you start a body of code,
there's no multithreading in the language.
There's no way for you to sort of fork off some other process.
is doing it as a built-on.
So browsers with their set timeout and their set interval.
That's all happening outside of the core language semantics.
and the interpreter and runtimes read it exactly
and run it exactly that way.
it goes from top to bottom, and that'll become important,
as we see in just a minute.
That means that you can change nearly everything,
and we'll talk about the several exemptions to this rule,
but those exemptions are very small,
and they're very narrow, but they wind up being powerful.
So that means that if you get an object back from
some function call,
Closure is the way we do private state, though,
so if I get an object,
and everything is mutable,
it means, ah, I might be able to surprise somebody else.
I might be able to go change some state out
from underneath them.
The thing that returned me the object might expect it
back in some reasonably okay state.
isn't through the private key order,
through some method that gives you some sort of a private field
that you can only see.
Instead, we invert the relationship
between classes and functions,
and we treat functions as behavior that can carry data,
which are data that can carry behavior.
And so the last key concept is, instead of having a class,
as you understand it in other languages,
we have prototypes,
which is to say we don't have this strong "is a" relationship
through a hierarchy of classes.
Instead, we say, when I don't find it here,
please look over there.
It's delegation, and it's a one-link chain delegation
up to a root object that everybody shares.
So we'll talk a lot about how exactly all this works,
how it fits together,
and hopefully you'll understand at the end
how it's going to inform where we can go from here.
one line before the next,
before the next--because there isn't necessarily
a compile cycle.
That means that the easiest way
is as something that is going to happen in a live environment.
So if you had a command line,
and you started typing in commands,
Top to bottom, line for line,
it gets evaluated in the order that it is written out
in the program, more or less.
And so statement to statement,
your programs--they can change in ways that are surprising
in other languages.
Where you might otherwise have compile time exceptions,
and runs the next line.
it's important to think about it simply as running at the top
and going to the bottom.
It's not really some big magical machine
that's going to be out there doing something for you,
and then it's going to start running your program.
It's just running top to bottom.
and you're trying to figure out what's going on,
remember that the line before it may be the thing
that caused the problem.
It's really important to think of functions
That means that they're not simply a pointer out
in the world that you invoke against something.
They are actual objects.
I'm going to refer to them through this talk
as function objects,
because function objects are indeed objects that you can go
and hang behavior off of.
But you don't hang behavior off of them
by extending their public API area.
You don't say, "I've got a function object.
I'm going to add some new property to it."
Most of the time, you do that by using functions as scopes.
is to invoke a function.
When you invoke a function, it sort of creates a new scope.
If statements, while statements, for-in statements--
those things don't create scopes.
Only functions do, so we have this problem of
how do we do beta hiding?
Well, these scopes are really smart,
because these scopes hold on to the variables
that have been defined above them in another scope.
So what we've got here is a function called get counter,
and get counter defines a local variable, I,
and it returns another function,
and that function references the variable I inside of it.
In other languages like C++ or Java,
you really can't do anything here,
because that variable I is going to go out of scope
in the return function.
We're allocating a new function here,
but that inner function is going to hold on to I.
It actually allocates private memory to store a reference
to I on the function object
that's returned out of this statement here.
It, again, inverts the relationship.
It's not storage with behavior.
It's behavior that has hidden storage.
And so the way we do private variables
is to use this idea of a closure, something that encloses
its lexical scope
and holds on to variables
as a way of passing behavior around.
So we can call the get counter function.
It hands us back a function object.
We can call it multiple times, and that state isn't gone.
We can still see the variable I from inside the outside one,
but it isn't referenceable.
We can't go and inspect some property on that function object
and find out which variables it's holding references to.
It gets GC'd just like everything else in the language.
So these are first class functions.
These are functions that are things in the system.
They're actually objects. You can create them.
You can add properties to them. It can enclose scope.
They're not simply inert bodies of code that get run.
They're participants in the object model.
They're participants in the storage system.
You can use them--I know the fundamental concept
that underpins a lot of the patterns
that we're going to see later.
So these functions work together
with a lot of other sort of functional ideas
about how a program language can be structured.
and for each method on the array prototype,
which means that every array in the system has these methods,
which means that instead of having an external iterator,
you have an internal iterator.
You have something that can call a function
across some set of arguments,
and so you wind up creating a stack of stuff
that you'd like to do in terms of behavior.
Instead of passing data structures around
and around and around, you pass in arguments to functions,
and that sort of unwinds the thing
that you were trying to get done.
You express your program's intent
in the form of nested functions
that are going to unwind to some result,
not linear code that's going to be executed
by passing in the same data structures
over and over and over again.
So these sorts of things are not hard and fast rules
about any programming language.
Like, you can have an endless debate about
what makes something a functional programming language.
Can it have side effects?
Can it not have side effects? You know--
How completely does it support some particular
set of macro languages, or hygienic macros,
or whatever it is?
Many people define functional languages differently,
but for the intents and purposes here,
we're just going to say it has closures,
first class functions,
and some concept of using those sorts of things
to compose behavior nicely together.
is that everything is just an object.
and so you can think of it as sort of a lazy language design.
It doesn't really have a lot of specialized,
to hold on to different concepts
that you might encounter.
Instead, it just relies on the same systems
over and over again.
One of those systems is this small type system,
where objects are objects, that first object literal there.
It's an instance of object. Arrays are objects,
which means that arrays are instances of objects, too.
And functions are also objects.
In this case, I've got a paren here,
which is going to create a new expression.
I've got a function, which I define inside of here.
It doesn't have a name. It's an anonymous function.
And the result of this expression
is just going to be that function object.
And the function object also is an instance of the object type,
which means that nearly everything in the system
that you encounter is going to be an object.
This is really powerful, because it means,
as we'll see later,
when we compose things, and everything is mutable,
we can start to change the behavior
of large parts of the system all at once.
And every object in the system acts more or less like a map.
So if you want a map, just take an object.
This is where-- sort of where JSON comes from,
this object literal syntax that we've got here,
where we're defining an object
with a single property and a single value.
It allows us to de-reference properties the same way.
So the data operator does almost exactly the same thing
as this map operator.
It just finds a property by its name
and returns it out the other side.
Objects operate like maps. That's pretty cool.
Arrays do exactly the same thing.
And arrays are very confusing when you start out working
because you think array is some separate thing
over on the side. Arrays are not objects.
Arrays are this linear bag of memory
that you're going to access with an integer someplace.
And as a result of that,
you're not going to be surprised by some other identity.
But you are, because very often what happens is
you go and extend an object. You add a new property to it.
In this case, we're going to add this other greeting property
to an object dynamically, and-- or to an array dynamically.
And as a result, when we go into a for-in loop,
this is now an innumerable property on this array.
We might get surprised, because we see these other things
showing up in our object.
Well, that's weird.
I mean, we iterated over the public properties.
Isn't 0 just an integer index thing?
It's not a public property, right?
just sort of falls back on these core concepts.
And if you think about the array integer indexing
working exactly the same way that property indexing does,
it all makes sense.
Yeah, in the implementation,
there might be some special machinery to make arrays
efficient or to pack them tightly,
so that you don't wind up slowing things down
But in the language semantics,
what happens here when I say,
"Please give me item 0 out of this list,"
is that it turns that 0 into a string,
and then does a map lookup.
That's all it does.
So the only magical thing about arrays
versus any other kind of object in the system
is that, when I push onto an array
or I set the length property,
it actually affects which properties are visible.
The length property is the only thing in an array
that's actually magic.
It has a little bit of syntax for defining arrays naturally;
but other than that, arrays are just objects.
Things get turned into strings and then de-referenced that way.
The spec is pretty clear about this.
And if you understand them, you can understand
what's going on in your system.
So we've got mutable objects. We've got closures,
which are behavior that carries data
and not data that carries behavior.
We've got mutable objects,
and we've got everything being an object
and everything being an object also being a map.
Okay, that's not a lot of concepts, so far,
for a programming language.
And we can use these to build some really powerful stuff.
So I mentioned mutability,
and I said everything is mutable.
Just a really quick example, we can add new properties
at runtime to every object.
When you're reading a program like this,
it's not like my object type somehow was extended
and, therefore, every object of this type is going to have
one of these properties.
I'm just adding a new property to the object directly.
So in this case, object.item is being replaced,
and object.item2 is simply being added.
These are exactly the same operations,
The dot operator just finds you the object
and then assigns to it. That's all it's doing.
Every object in the system is extensible.
Most of them are mutable.
Most of the values are mutable, and we run top to bottom.
So that means that when I come down here,
and I delete a property off of the object,
the very next line isn't going to see it.
But if I had said console.log(obj.item)
one line above, it would see it there.
This is not a compile time thing.
It's just doing what you said, line after line,
statement after statement, expression after expression.
It seems really simple.
It seems pretty obvious, but very few programming languages
that you might be using in a compiled environment
work this way.
So I mentioned that closures are the other side of classes,
whereas classes are sort of a nice way
of saying, "Here's a structure of data.
"I'm going to associate some properties with them,
and maybe they'll have some type behavior as well."
In this case, we're going to create something
We know it's a class,
because it's got an uppercase B for the name.
This is not a language enforced semantic.
This is just a convention. And as we'll see, conventions
So remember every object is mutable.
So we're going to create down here,
we're going to create a new instance of our behavior class.
We're going to extend the local object, this dot,
with the variable that was passed in the configuration,
and then we're going to extend the object again
with a function called do it.
In this case,
when I call the do it method of my behavior instance,
it's then going to run through,
and it's going to say this.config
and go grab some flag off the configuration.
Okay, so we stored some property and some behavior on the object.
This looks a lot like what you might expect out
of another object-oriented language.
The declaration syntax is a little bit funky,
but you sort of understand it, right?
I've added a method. I've added some data.
The data operates on the local objects method.
Sweet-- or other way around.
Strike that, reverse it.
New behavior, passing a flag, then I call it false.
I get a behavior object,
and now I can call a method that uses that behavior, right?
So it's going to look at the local object, this,
for the configuration, and it may change its behavior
based on that configuration by passing some other value to it.
Make sense? Cool? All right.
It's the flip side of that.
Instead of creating a class that I created an instance of,
I'm going to create a generator
that's going to pass me back a function object,
which is going to hold on to the state.
I'm not going to go create a class for it.
I'm going to create a behavior generator.
You can think of them doing the same thing,
just the flip side of it.
So instead of saying new behavior,
I'm going to say bind me a behavior,
which when I call this,
note the lack of the new keyword here,
I'm going to pass into configuration.
Like we saw earlier,
I'm going to pass back out a function object.
This is a new function object.
Every time I call this method, bind behavior,
it's going to pass me back out a new function object.
So I'm actually having a new function object allocated here,
and that function object is going to have, again,
some private storage, and that private storage is
going to hold on to the config variable
that was passed into the outer function, right?
Because each one of these is a new scope,
and because scopes can hold on to the variables
that they were able to see when they were defined,
the function object that gets passed out of here now
has a reference.
It's holding on to that local state.
In this case, it's going to be the object that was passed in.
This might go out of scope here in every other place
in my program.
I may not be able to get a reference back to this object,
but my behavior, the B variable that was passed back out,
will have access to that data
because it's being held on to internally.
It's not going to be garbage collected out
from underneath me.
Closures are the way to invert the way you think
about your programs.
You don't create classes that are state with data,
state with behavior attached.
You create behavior that holds on to the state that it needs,
and you can pass that behavior around,
because functions are first class.
So I mentioned earlier that the last sort of big conceit
in the language is that we don't have a way of saying,
hey, here's a class of stuff.
Instead we say, if you don't find it here,
don't look at my like chain of class inheritance.
Instead, just go look at that other object.
Remember how I said over and over again
I get some code.
The thing executes front to back, top to bottom.
We're going to see the exact same thing here,
because what happens every time you call the .operator
is exactly the same thing.
I'm going to create some variable
that I'm going to call my delegate.
It's an object. It's got a single property.
There's a new ECMAScript 5 method called object.create.
There's other ways to do this in older versions,
but they're a little bit mind bending,
and we won't go over them.
But object.create-- the easiest way to think of it
is that it creates a new object which will look at the object
that you pass as its first property
if you don't find a value with this same property name
on that object.
So let's say--and in this case, I'm going to create object 2
and have my delegate be the delegate.
And so when I reference a property out of object 2,
it's going to go look it up dynamically,
and if it doesn't find it on object 2 directly,
it's going to go look it up off of the delegated 2 object.
It's going to go, dynamically go
and try and find it over there.
Well, we created another object.
In this case, we're setting a local property on that object,
whose name is item, whose value is value.
And that means that when I look it up at runtime
directly top to bottom, left to right,
what I see is that I don't get the value
that was set on my delegate.
I get the value that was on the local object.
The .operator doesn't fail on the local lookup.
It finds it on the local object.
And instead of looking up the chain, it says,
ah, I'll just give you this object's value right back.
when it comes even to looking up properties on objects.
There's not some fixed list of stuff that you can do.
You can change the delegation,
and you can change the properties
that are available on every object
that you're delegating to, or your local object,
and that changes what happens when you go
and look stuff up at the very next line.
So in this case,
if I go and I change the value on the delegate,
I change the item value on the delegate,
remember that object 2 doesn't have a local property
If I fail on that lookup on object 2,
it just goes and says, ah, okay, let's go consult my delegate,
and that delegate is now going to have the new value.
So the new value has been shadowed all the way
through to everything else in the system
that is delegating to that object.
This is incredibly powerful.
because it reads top to bottom,
because almost everything is mutable,
and because I can delegate to most other objects
when I create something,
I wind up in a place where I can create brand-new behavior
I can compose things on the next line that didn't exist before,
or I can change the behavior of other objects
in the system,
based on what they're delegating to.
This turns out to be a great way
to go build up a lot of the constructs
that we get in other languages for ourselves,
This power is the sort of thing that really drew me
I didn't really understand what I was dealing with.
I remember I had a friend who told me,
after I'd written some article about how to do
signals and slots, some sort of aspect-oriented
event style thing, he's like,
"Well, why didn't you just use a closure for that?"
And it took me a long time,
probably six months or more after that,
to sort of really understand what it was
that he meant when he said, "Just use a closure for that."
I didn't understand that you could hide data
inside of functions.
ran top to bottom.
But these core concepts allow you to create
all sorts of really powerful stuff,
assuming we understand what happens with the word "this."
So the word this is really special.
In order--Because we don't have classes that wire up
this inheritance hierarchy,
and because we're always delegating at runtime, right,
every .operator sort of does the dynamic lookup
on the local object--
looks at its delegate, looks at its delegates.
The this object is a way of saying,
okay, whatever scope I'm in,
execute the next lookup against the local object,
which means that the this keyword in any function
isn't pointing at some fixed object.
It's not fixed when I necessarily say,
you know, create me an object. It's promiscuous.
The this keyword points at whatever object my function
is being called through.
Right, remember function objects are first class?
They don't actually sort of carry around relationships
to their class or the thing that they were defined in.
They hold their own data.
So the this keyword is a nice little syntactic out
which lets you say, okay, whenever I look up a property,
which happens to be a function,
and I call it, the .operator for method calls says
don't just return it.
But if I evaluate it directly, use the .call property
of the function object
that's returned and call it in the scope of the object
on the left hand side of the .operator.
I know this is a little bit maybe tricky,
but the easiest way to think about this is that in order to
wire up this behavior correctly,
so that it sort of does what you expect out of other languages,
we rely on the function being first class,
meaning it has its own call and apply methods.
You don't say necessarily, hey, function,
you're not going to work if you're not called inside
of some other object.
I can call any function in the scope of any other object.
I can assign a function to any other object and then call it
through that object dynamically, right?
Everything is mutable.
Functions are first class. Why not?
So in order to get that to execute
against the right object,
you use the this keyword to go grab
the value out of the thing
that was on the left hand side of dot,
which is exactly the same thing as saying
please call my function,
which I pulled out of that object,
in the scope of the object on the left hand side.
So all this fits together in ways that allow us
to recreate a lot of this stuff that we expected
in other languages.
A lot of this is convention.
A lot of this isn't necessarily the sort of thing
that you're going to have language level
or syntactic support for,
but if you understand what's going on there,
those sort of core little ideas about mutability,
scope, functions as first class citizens, and dynamic behavior,
we can start to recreate things like classes.
So here we've got an item type,
which is we're going to think of as a class.
It's not really called a class.
It's just a function. It's got the word function.
As you saw earlier,
when you use the word new in front of any function,
it sort of creates an object,
calls that function inside the context of that object,
and then returns that object back to you.
That's the way to think about the new keyword.
So we're just going to create a function,
which we're going to call with new sometime later,
and inside of it we're going to execute
a couple of other functions.
Now, we just saw .call, and .call calls
those other functions in the scope of the object
that we're being executed in or that we want to pass in.
In this case, we're going to pass in the local object,
and we're going to do it twice,
which means that inside of tract,
we're going to assign a new property to this--
which is to say the object that was passed in--
and inside of logged, we're going to assign a new method
where that method is going to go dynamically look up the ID
and log it out.
So we call these mix-ins.
These two methods up here were written in a way
that they don't delegate to anything else.
They don't assume anything about the behavior of their methods
or the properties that they define.
And instead, they just add stuff.
They just add stuff dynamically
when they're invoked against some other object.
So I could call them in any other context,
but in this case, I can use them in the item type constructor
to extend the item type with some new stuff.
We saw earlier how delegation allows you to
create new delegate relationships between things,
and functions also have this idea of a prototype.
This is the exposed version of the thing we saw
before with object.create,
where I can say, please wire up this relationship
so that this object, when you don't find it here,
looks at that thing over there.
So if we create a new item type,
what we'll see is that it has an ID,
and it has a type associated with property added to them,
which is pretty good. And if we create another one,
the counter is incremented,
and the type is still assigned to the same value.
But if we create a new sub item type,
what we see is that, because the prototype created
a new property called type with a new value,
again, the delegate system faults on the local object,
looks at the object's prototype--in this case,
SubItemType.prototype-- pulls it out of there
and doesn't fault all the way through to ItemType.prototype.
So we can compose these things together in a way
that gives us something like classical inheritance.
It's not exactly the same thing.
All this is dynamic.
I can go and change these prototypes.
I can change these objects later.
But I've got the ability to factor out code
into something like a macro or a trait, using mix-ins,
and I've got the ability to create
an entire subclass relationship,
where I define new function types
which defer to their super classes
for a lot of their behavior.
All right, we're starting to get someplace.
These core concepts have given us the ability to
define things that the language didn't give us naturally,
but we can go get for ourselves.
You know how I said that everything is mutable,
more or less?
That means that we can go and extend the stuff
that is deferred to by almost every object
in the system, right?
Remember, arrays are objects.
Objects are objects. Functions are objects.
Well, those things have prototypes, too.
They have exposed objects,
which are--they defer to when you don't find a property
in the local object.
So a radar prototype is, again, an object which is mutable,
and every instance of array, every array in the system
that faults on some property is going to go look it up off
of this object instead. So I can extend it.
I can say array.prototype.clear is a new function.
I can extend every single array in the system at runtime,
because the next line is going to look it up
through exactly the same mechanism
as everything else, right?
This is not me changing the type of array.
I'm just extending it dynamically.
I'm creating a new thing for you to hit,
when you don't find the property in the local array object.
And I'm going to add to.
In this case, they're going to return new arrays
when they're done, and so I can chain them together.
And so this is how you can sort of create
by changing the things that you delegate to.
If you create new objects,
and they delegate to some prototype,
and you can mutate that prototype,
well, then you can change almost everything about the system.
Yes, you can change almost everything about the system.
And, yes, it is a huge maintainability nightmare.
This is really good when your code can do it.
This can be a huge problem when everybody's code can do it,
especially as you get into larger code bases.
So collectively, as a community,
of practice, which they say, please don't ever go
and mutate array.prototype.
Please don't change object.prototype,
so that you don't wind up
stepping on somebody else's toes,
stepping on somebody else's extensions.