官术网_书友最值得收藏!

Functional object-oriented programming

JavaScript is a functional object-oriented programming language. However, it is quite different to other object-oriented programming languages such as C# or Java. Despite having a similar syntax, there are some important differences.

Functional programming in JavaScript

In JavaScript, functions are first-class objects. This means that functions can be treated like any other object: they can be created dynamically, assigned to variables, or passed into methods as arguments.

This makes it very easy to specify event callbacks, or to program in a more functional style using higher-order functions. Higher-order functions are functions that take other functions as arguments, and/or return another function. Here's a trivial example of filtering an array of numbers first in an imperative style and then in a functional style. Note that this example also shows JavaScript's array literal notation for creating arrays, using square brackets. It also demonstrates JavaScript's conditional construct and one of its loop constructs, which should be familiar from other languages:

var numbers = [1,2,3,4,5,6,7,8];

var filteredImperatively = [];
for (var i = 0; i < numbers.length; ++i) {
    var number = numbers[i];
    if (number % 2 === 0) {
        filteredImperatively.push(number);
    }
}
console.log(filteredImperatively); // Prints [2, 4, 6, 8]

var filteredFunctionally =
    numbers.filter(function(x) { return x % 2 === 0; });
console.log(filteredFunctionally); // Prints [2, 4, 6, 8]

The second approach in the example makes use of a function expression to define a new, anonymous function inline. In general, this is referred to as a lambda expression (after lambda calculus in mathematics). This function is passed-in to the built in filter expression available on JavaScript arrays.

In C#, assignment and passing of behavior was originally only possible using delegates. Since C# 3.0, support for lambda expressions makes it much easier to use functions in this way. This allows a more functional style of programming, for example, using C#'s Language-Integrated Query (LINQ) features.

In Java, for a long time there was no native way for a function to exist independently. You would have to define a method on a (possibly anonymous) class and pass this around, adding a lot of boilerplate. Java 8 introduces support for lambda expressions in a similar way to C#.

While C# and Java may have taken a while to catch up, you might be thinking that JavaScript is now falling behind. The syntax for defining a new function in JavaScript is quite clumsy compared to the lambda syntax in C# and Java.

This is especially unfortunate since JavaScript uses a C-like syntax for familiarity with other languages like Java! This is resolved in ES2015 with arrow functions, allowing us to rewrite the previous example as follows:

var numbers = [1,2,3,4,5,6,7,8];
var filteredFunctionally = numbers.filter(x => x % 2 === 0);
console.log(filteredFunctionally); // Prints [2, 4, 6, 8]

This is a simple arrow function with a single argument and a single expression. In this case, the expression is implicitly returned.

Note

It can be useful to read the => notation in arrow functions as goes to.

Arrow functions may have multiple (or zero) arguments, in which case they must be surrounded by parentheses. If the function body is enclosed in braces, it may contain multiple statements, in which case there is no implicit return. These are exactly the same syntax rules as for lambda expressions in C#.

Here is a more complex arrow function expression that returns the maximum of its two arguments:

var max = (a, b) => {
    if (a > b) {
        return a;
    } else {
        return b;
    }
};

Understanding scopes in JavaScript

Traditionally, in JavaScript, there are only two possible variable scopes: global and functional. That is, an identifier (a variable name) is defined globally, or for an entire function. This can lead to some surprising behavior, for example:

function scopeDemo() {
    for (var i = 0; i < 10; ++i) {
        var j = i * 2;
    }
    console.log(i, j);
}
scopeDemo();

In most other languages, you would expect i to exist for the duration of the for loop, and j to exist for each loop iteration. You would therefore expect this function to log undefined undefined. In fact, it logs 10 18. This is because the variables are not scoped to the block of the for loop, but to the entire function. So the preceding code is equivalent to the following:

function scopeDemo() {
    var i, j;
    for (i = 0; i < 10; ++i) {
        j = i * 2;
    }
    console.log(i, j);
}
scopeDemo();

JavaScript treats all variable declarations as if they were made at the top of the function. This is known as variable hoisting. Although consistent, this can be confusing and lead to subtle bugs.

ES2015 introduces the let keyword for declaring variables. This works exactly the same as var except that variables are block-scoped. There is also the const keyword, which works the same as let except that it does not allow reassignment. It is recommended that you always use let rather than var, and use const wherever possible. Check the following code for example:

function scopeDemo() {
    "use strict";
    for (let i = 0; i < 10; ++i) {
        let j = i * 2;
    }
    console.log(i, j); // Throws ReferenceError: i is not defined
}
scopeDemo();

Note the "use strict" string in the preceding example. We'll discuss this in the next section.

Strict mode

The "use strict" string is a hint to the JavaScript interpreter to enable Strict Mode. This makes the language safer by treating certain usages of the language as errors. For example, mistyping a variable name without strict mode will define a new variable at the global level, rather than causing an error.

Strict mode is also now used by some browsers to enable features in the newest version of JavaScript, such as the let and const keywords previously shown. If you are running these examples in a browser, you may find that the preceding listing doesn't work without strict mode.

In any case, you should always enable strict mode in all of your production code. The "use strict" string affects all code in the current scope (that is, JavaScript's traditional functional or global scope), so should usually be placed at the top of a function (or the top of a module's script file in Node.js).

Object-oriented programming in JavaScript

Anything that is not one of JavaScript's built-in primitives (strings, number, null, and so on) is an object. This includes functions, as we've seen in the previous section. Functions are just a special type of object that can be invoked with arguments. Arrays are a special type of object with list-like behavior. All objects (including these two special types) can have properties, which are just names with a value. You can think of JavaScript objects as a dictionary with string keys and object values.

Objects can be created with properties using the object literal notation, as in the following example:

var myObject = {
    myProperty: "myValue",
    myMethod: function() {
        return `myProperty has value "${this.myProperty}"`;
    }
};
console.log(myObject.myMethod());

You might find this notation familiar even if you've never written any JavaScript, as it is the basis for JSON. Note that a method is just an object property that happens to have a function as its value. Also note that within methods, we can refer to the containing object using the this keyword.

Finally, note that we did not need to define a class for our object. JavaScript is unusual amongst object-oriented languages in that it doesn't really have classes.

Programming without classes

In most object-oriented languages, we can declare methods in a class for use by all of its object instances. We can also share behavior between classes through inheritance.

Let's say we have a graph with a very large number of points. These may be represented by objects that are created dynamically and have some common behavior. We could implement points like this:

function createPoint(x, y) {
    return {
        x: x,
        y: y,
        isAboveDiagonal: function() {
            return this.y > this.x;
        }
    };
}

var myPoint = createPoint(1, 2);
console.log(myPoint.isAboveDiagonal()); // Prints "true"

One problem with this approach is that the isAboveDiagonal method is redefined for each point on our graph, thus taking up more space in memory.

We can address this using prototypal inheritance. Although JavaScript doesn't have classes, objects can inherit from other objects. Each object has a prototype. If we try to access a property on an object and that property doesn't exist, the interpreter will look for a property with the same name on the object's prototype instead. If it doesn't exist there, it will check the prototype's prototype, and so on. The prototype chain will end with the built-in Object.prototype.

We can implement this for our point objects as follows:

var pointPrototype = {
    isAboveDiagonal: function() {
        return this.y > this.x;
    }
};

function createPoint(x, y) {
    var newPoint = Object.create(pointPrototype);
    newPoint.x = x;
    newPoint.y = y;
    return newPoint;
}

var myPoint = createPoint(1, 2); 
console.log(myPoint.isAboveDiagonal()); // Prints "true"

The isAboveDiagonal method now only exists once in memory, on the pointPrototype object.

When we try to call isAboveDiagonal on an individual point object, it is not present, but it is found on the prototype instead.

Note that this tells us something important about the this keyword. It actually refers to the object that the current function was called on, rather than the object it was defined on.

Creating objects with the new keyword

We can rewrite the preceding code example in a slightly different form, as follows:

var pointPrototype = {
    isAboveDiagonal: function() {
        return this.y > this.x;
    }
}

function Point(x, y) {
    this.x = x;
    this.y = y;
}

function createPoint(x, y) {
    var newPoint = Object.create(pointPrototype);
 Point.apply(newPoint, arguments);
    return newPoint;
}

var myPoint = createPoint(1, 2);

This makes use of the special arguments object, which contains an array of the arguments to the current function. It also uses the apply method (which is available on all functions) to call the Point function on the newPoint object with the same arguments.

At the moment, our pointPrototype object isn't particularly closely associated with the Point function. Let's resolve this by using the Point function's prototype property instead. This is a built-in property available on all functions by default. It just contains an empty object to which we can add additional properties:

function Point(x, y) {
    this.x = x;
    this.y = y;
}

Point.prototype.isAboveDiagonal = function() {
    return this.y > this.x;
}

function createPoint(x, y) {
 var newPoint = Object.create(Point.prototype);
    Point.apply(newPoint, arguments);
    return newPoint;
}

var myPoint = createPoint(1, 2);

This might seem like a needlessly complicated way of doing things. However, JavaScript has a special operator that allows us to greatly simplify the previous code, as follows:

function Point(x, y) {
    this.x = x;
    this.y = y;
}

Point.prototype.isAboveDiagonal = function() {
    return this.y > this.x;
}

var myPoint = new Point(1, 2);

The behavior of the new operator is identical to our createPoint function in the previous example. There is one small exception: if the Point function actually returned a value, then this would be used instead of newPoint. It is conventional in JavaScript to start functions with a capital letter if they are intended to be used with the new operator.

Programming with classes

Although JavaScript doesn't really have classes, ES2015 introduces a new class keyword. This makes it possible to implement shared behavior and inheritance in a way that may be more familiar compared to other object-oriented languages.

The equivalent of our preceding code would look like the following:

class Point {
    constructor(x, y) {
        this.x = x;
        this.y = y;
    }
    
    isAboveDiagonal() {
        return this.y > this.x;
    }
}

var myPoint = new Point(1, 2);

Note that this really is equivalent to our preceding code. The class keyword is just syntactic sugar for setting up the prototype-based inheritance already discussed.

Class-based inheritance

As mentioned before, an object's prototype may in turn have another prototype, allowing a chain of inheritance. Setting up such a chain becomes quite complicated using the prototype-based approach from the previous section. It is much more intuitive using the class keyword, as in the following example (which might be used for plotting a graph with error bars):

class UncertainPoint extends Point {
    constructor(x, y, uncertainty) {
        super(x, y);
        this.uncertainty = uncertainty;
    }
    
    upperLimit() {
        return this.y + this.uncertainty;
    }
    
    lowerLimit() {
        return this.y - this.uncertainty;
    }
}

var myUncertainPoint = new Point(1, 2, 0.5);
主站蜘蛛池模板: 阳春市| 芒康县| 柏乡县| 巴彦淖尔市| 云林县| 新干县| 遵义县| 沾化县| 镇坪县| 九江县| 富民县| 阿城市| 黄石市| 通江县| 洮南市| 阿克| 寿阳县| 普兰店市| 六安市| 岚皋县| 周至县| 宝应县| 汝城县| 河南省| 寻乌县| 西乡县| 沂水县| 辽中县| 太和县| 漳浦县| 漳平市| 佛冈县| 团风县| 岱山县| 灌云县| 永善县| 讷河市| 屏东市| 自贡市| 临桂县| 乌拉特后旗|