How do you program in JavaScript?
- … supporting object-oriented, imperative, and functional programming
- (面向对象、命令式、函数式)
- Originally programming conventions (i.e. patterns ) rather than language features
- ECMAScript adding language features (e.g.
class
,=>
, etc. )
- ECMAScript adding language features (e.g.
Object-oriented programming: methods
- With first class functions a property of an object can be a function
let obj = {count: 0};
obj.increment = function (amount) {
this.count += amount;
return this.count;
}
- Method invocation: calls function and binds this to be object
obj.increment(1); // returns 1
obj.increment(3); // returns 4
this
- In methods this will be bound to the object
let o = {oldProp: 'this is an old property'};
o.aMethod = function(){
this.newProp = "this is a new property";
return Object.keys(this); // will contain 'newProp'
}
o.aMethod(); // will return ['oldProp', 'aMethod', 'newProp']
- In non-method functions :
- this will be the global object
- Or if
use strict
; this will beundefined
functions are objects - can have properties(属性)
function plus() {
if (plus1.invocations == undefined) {
plus1.invocations = 0;
}
plus1.invocations++;
return value + 1;
}
plus1.invocations
will be the number times(次数) function is called- Acts like static/class properties in object-oriented languages
functions are objects: have methods
function func(arg) {
console.log(this,arg);
}
toString()
method - return functions as source stringfunc.toString() // returns 'function func(arg) { console.log(this,arg);}'
call()
method - call function specifyingthis
and argumentsfunc.call({t: 1},2) // prints '{t: 1} 2'
- apply( ) like call( ) except arguments are passed as an array -
func.apply({t: 2}, [2])
this
is like an extra hidden arguments to a function call and is used that way sometimes
blind()
method - creates a new function with this and arguments boundlet newFunc = func.blind({z: 2}, 3);
newFunc() //prints '{z: 2} 3'
Object oriented programming: class
- Functions are classes in JavaScript: Name the function after class
function Rectangle(width, height) {
this.width = width;
this.height = height;
this.area = function() { // not correct way of adding methods
return this.width*this.height;
}
}
let r = new Rectangle(26, 14); // {width: 26, height:14}
Functions used in this way are called constructors(构造函数) :
r.constructor.name == 'Rectangle'
console.log®: Rectangle { width: 26, height: 14, area: [Function] }
Object-oriented programming: inheritance
-
JavaScript has the notion of a prototype(原型) object for each instance(实例)
- prototype objects can have prototype objects forming a prototype chain(原型链) (原型对象可以拥有形成原型链的对象)
- obj --> proto -->proto --> … -->proto -->null
-
On an object property read access JavaScript will search the up the prototype chain until the property is found(在对象属性的读取和访问上,JS会沿着原型链向上搜索,直到找到该属性)
- Effectively the properties of an object are its own property inaddtion to all the properties up the prototype chain. This is called prototype-based inheritance. (实际上,一个对象的所有属性是它自己的属性加上所有原型链上的属性。这就是原型链继承)
-
Property updates are different: always create property in object if not found. (属性更新却不同:如果没找到属性,会在对象里创建该属性)
Using prototypes
function Rectangle(width, height) {
this.width = width;
this.height = height;
}
Rectangle.prototype.area = function() {
return this.width*this.height;
}
let r = new Rectangle(26, 14); // {width: 26, height: 14}
let v = r.area(); // v == 26*14
Object.keys(r) == ['width', 'height'] // own properties
Note: Dynamic - changing prototype will cause all instances to change. (改变原型会改变所有实例)
Prototype versus object instances
let r = new Rectangle(26, 14);
Understand the difference between:
r.newMethod = function() { console.log('New Method called'); }
And:
Rectangle.prototype.newMethod = function() {
console.log('New Method called');
}
Inheritance
Rectangle.prototype = new Shape(...);
- If desired property not in
Rectangle.prototype
then JavaScript will look inShape.prototype
and so on.- Can view prototype objects as forming a chain. Lookups(查找) go up the prototype chain.
- Prototype-based inheritance (基于原型的继承)
- Single inheritance support (单继承支持 ?啥是单继承)
- Can be dynamically created and modified (可以动态创建和修改)
ECMAScript version 6 extensions
class Rectangle extends Shape { // Definition and Inheritance
constructor(height, width) {
super(height, width);
this.height = height;
this.width = width;
}
area() { // Method definition
return this.width*this.height;
}
static countRects() { // static method
...
}
}
let r = new Rectangle(10, 20)
React.js example class
class HelloWorld extends React.Component {
constructor(props) {
super(props);
...
}
render() {
return (
<div>Hello World</div>
);
}
}
class HelloWorld extends React.Component {
constructor(props) {
super(props);
this.clickHandler = this.clickHandler.blind(this); // what does this do?
...
}
clickHandler() {
...
}
render() {
return(
<div onClick={this.handleClick}>Hello World</div>
);
}
}
Functional Programming
for (let i = 0; i < anArr.length; i++) {
newArr[i] = anArr[i]*i;
}
newArr = anArr.map(function(val, ind){
return val*ind;
});
anArr.filter(filterFunc).map(map.Func).reduce(reduceFunc)
Functional Programming - ECMAScript 6
for (let i = 0; i < anArr.length; i++) {
newArr[i] = anArr[i]*i;
}
newArr = anArr.map((val, ind) => val*ind); // Arrow function
anArr.filter(filterFunc).map(mapFunc).reduce(reduceFunc);
Arrow functions don’t redefine this
We can mostly but not totally avoid functional style
function callbackFunc() { console.log("timeout"); }
setTimeout(callbackFunc, 3*1000);
- Server:
function callbackFunc(err, data) { console.log(String(data)); }
fs.readFile('/etc/passwd', callbackFunc);
-
Node.js programming: Write function for HTTP request processing
-
React’s JSX prefers functional style: map(), filter(), ?:
Closures (闭包)
let globalVar = 1;
function localFunc(argVar) {
let localVar = 0;
function embedFunc() { return ++localVar + argVal + globalVar;}
return embedFunc;
}
let myFunc = localFunc(10); // What happens if a call myFunc()? Again?
myFunc
closure containsargVar
,localVar
andglobalVar
Using Scopes(作用域) and Closures
var i = 1; // i is global
...
function() {
i++;
return i;
}
versus
return (function() {
var i = 1; //not global
...
function f() {
i++;
return i;
}
return f;
})();
Using closures for pravite object properties
let myObje = (function() {
let privateProp1 = 1; let privateProp2 = "test";
let setPrivate1 = function(vall) { privateProp1 = vall;}
let compute = function() {return privateProp1 + privateProp2;}
return {compute: compute; setPrivate1: setPrivate1};
}) ();
typeof myObj; // 'object'
Object.keys(myObj); // ['compute', 'setPrivate1']
What does myObj.compute()
return?
Beware of this
and nested functions
'use strict';
function readFileMethod() {
fs.readFile(this.fileName, function (err, data) {
if(!err) {
console.log(this.fileName, `has length`, data.length);
}
});
}
let obj = {fileName: "aFile"; readFile: readFileMethod};
obj.readFile();
- Generates error on the
console.log
state sincethis
is undefined
Beware of this and nested functions - work around
'use strict';
function readFileMethod() {
fs.readFile(this.fileName, (err, data) => {
if(!err) {
console.log(this.fileName, 'has length', data.length);
}
});
}
let obj = {fileName: "aFile"; readFile: readFileMethod};
obj.readFile();
- Works since an arrow function doesn’t smash
this
Closures can be tricky with imperative code
// Read files './file0' and './file1' and return their length
for (let fileNo = 0; fileNo < 2; fileNo++) {
fs.readFile('./file' + fileNo, function (err, data) {
if(!err) {
console.log('file', fileNo, 'has length', data.length);
}
});
}
- Ends up printing two files to console both starting with:
file 2 has length
- Why?
Stepping through the execution
for (let fileNo = 0; fileNo < 2; fileNo++) { //execution starts here :fileNo = 0
fs.readFile('./file' + fileNo, function(err, data) { // 注释1&注释2&注释3:解释看下方
if(!err) { // 注释4
console.log('file', fileNo, 'has length', data.length); //注释5
}
});
}
- 注释1: call the function
fs.readFile
, before we can we must evaluate(评估) the arguments: the first argument results from the string concatenation operation forming'./file0'
, the second arguments is a function which is passed as a function and its closure containing the variables accessed by the function. In this case onlyfileNo
is accessed by the function so the closure containsfileNo
(which is currently 0) - 注释2: Note that
fs.readFile
returns after it has started reading the file but before it has called the callback function. The execution does thefileNo++
and calls back tofs.readFile
with an argument of'./file1'
and a new closure and function. The closure has onlyfileNo
(which is currently 1). - 注释3: After creating two function with closures and calling
fs.readFile
twice thefor
loop finishes. Some time later in the execution the file reads with finish andfs.readFile
will call the function we passed. Recall thatfileNo
is now 2. - 注释4:
'./file0'
is read so our callback starts executingerr
is falsy so we go to theconsole.log
statement - 注释5: When evaluating the arguments to
console.log
we go to the closure and look at the current value offileNo
. We find it as 2. The result we print the correctdata.length
but the wrong file number. The same thing happens for the'./fileNo1'
callback
Broken fix #1 - Add a local variable
for (let fileNo = 0; fileNo < 2; fileNo++) {
var localFileNo = fileNo;
fs.readFile('./file' + localFileNo, function(err, data) {
if(!err) {
console.log('file', localFileNo, 'has length', data.length);
}
});
}
- Closure for callback now contains
localFileNo
. Unfortunately when the callback functions runlocalFileNo
will be1
. Better than before since one of the printed lines has the correctfileNo
A fix - Make a private copy of fileNo
using a call
function printFileLength(aFileNo) {
fs.readFile('./file' + aFileNo, function (err, data) {
if(!err) {
console.log('file', aFileNo, 'has length', data.length);
}
});
}
for (let fileNo = 0; fileNo < 2; fileNo++) {
printFileLength(fileNo);
}
Note: This works but sometimes it prints the fileO
line first and sometimes it prints the file1
line first.
Another fix - Make a private copy of fileNo
with let
for (var fileNo = 0; fileNo < 2; fileNo++) {
let localFileNo = fileNo;
fs.readFile('./file' + localFileNo, function (err, data) {
if(!err) {
console.log('file', localFileNo, 'has length', data.length);
}
});
}
Note: Same out-of-order execution as previous fix
JavaScript Object Notation ( JSON )
let obj = { ps: 'str', pn: 1, Pa: [1, 'two', 3, 4], po: { spot: 1}};
let s = JSON.stringify(obj) =
'{"ps": "str", "pn": 1, "pa": [1, "two", 3, 4], "po": {"stop": 1}}'
typeof s == 'string'
JSON.parse(s) // returns object with same properties
- JSON is the standard format for sending data to and from a browser.
JavaScript: The Bad Parts
Declaring variables on use - Workaround: Force declarations
let myVar = 2*typeoVar + 1
Automatic semicolon insertion - Workaround: Enforce semicolons with checkersreturn "This is a long string so I put it on its own line";
Type coerceing equals: == - Workaround: Always use===
,!==
instead
("" == '0')
is false but(0 == '')
is true, so is(0 == '0')
and(false == '0')
is true as is(null == undefined)
with
,eval
- Workaround: Don’t use
Some JavaScript idioms
- Assign a default value
hostname = hostname || 'localhost';
port = port || 80;
- Access a prossibly undefined object property
let prop = obj && obj.propname;
- Handing multiple
this
:
fs.readFile(this.fileName + fileNo, function (err, data) {
console.log(this.fileName, fileNo); // wrong!
});
-the right way:
fs.readFile(this.fileName + fileNo, function(err, data) =>
console.log(this.fileName, fileNo)
);
- Handling multiple
this
:self
let self = this;
fs.readFile(self.fileName + fileNo, function(err, data) {
console.log(self.fileName, fileNo);
});