65.9K
CodeProject 正在变化。 阅读更多。
Home

JavaScript 重要概念速览

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.91/5 (47投票s)

2015年11月18日

CPOL

17分钟阅读

viewsIcon

37733

JavaScript 重要概念速览

引言

在本指南中,我试图将 JavaScript 的所有重要核心概念汇集在一起。学习 JS 对我来说非常困难,因为所有这些部分都散落在网络上。我多年来逐渐找到了它们。我希望我已涵盖了所有内容。如果您觉得我遗漏了重要内容或有任何不正确之处,请在评论中告诉我。

无论标题如何,本指南都应有助于所有开发人员。但是,在阅读本指南之前,您应该熟悉 JS 的基础知识。这更像是 JS 概念的集合。

松散类型

没错!无需绞尽脑汁决定这应该是 float 还是 doubleint 还是 shortlong 甚至是 String。只需使用 var my_variable; 声明变量,即可完成。这是最容易掌握的概念。

始终使用“use strict”;

您可以在函数或 JS 文件的开头使用神奇的短语 "use strict"; 来开启严格模式

就像任何其他语言一样,JS 语言的语法、语义和 API 首先在语言规范文档中提出;获得批准后,即可由所有浏览器实现。这有助于避免不同浏览器之间不兼容的实现,使我们这些 JS 开发人员的生活更轻松。不兼容的困扰在“DOM 不是 JS”一节中可见一斑。

对于 JS,语言规范文档名为 ECMAScript。我们现在在现代浏览器中看到的 JS 版本基于 ECMAScript 5。该规范描述了更严格的 JS 版本。坦率地说,非严格版本的 JS 允许并鼓励极其草率和糟糕的编码实践,最终会导致一个混乱不堪的产品。“strict”模式更加清晰,所有自尊自爱的 JS 开发人员都应该了解并使用它。

完整的限制列表可在 MDN 上找到,但我认为最重要的一点是,在 strict 模式下,所有变量必须在使用前声明。

因此,

function () {
    'use strict';
    return a;
}

如果在全局范围内尚未定义 a,则在 strict 模式下会导致错误。在非 strict 模式下,它将在全局范围内愉快地创建一个变量 a 并继续。稍后会详细介绍作用域。但是,我可以给您演示一下。

function f1() {
    a = 12;
    return a;
}
function f2() {
    return a;
}
alert(f1());
alert(f2());

尝试在 jsfiddle.net 上运行此代码。在这两个警报中,您将看到 12。这是因为在这两个函数中,a 都在全局作用域中。

另一个需要记住的重要一点是,一旦启用 strict 模式,就无法禁用。Strict 模式可以为特定的 JS 文件指定,在该文件中所有代码都将启用;或者,它可以在函数内部选择性地启用。

分号不是可选的

您之前可能在某个地方读到过,在 JS 中,语句末尾的分号不是必需的。但是,这并不意味着完全不需要分号。语言解释器实际上会尝试猜测分号应该在哪里,而您遗漏了它,然后继续执行。我非常讨厌这个“功能”。有时,这会导致难以发现的错误,而且在严格模式下也允许这样做。>:(

function avg(a, b) {
    return
        (a + b) / 2
}
console.log(avg(2, 3))

控制台上会打印什么?将是 undefined!像 C 或 Java 这样明确的语言在这里不会出错,但 JS 会,因为它过于热衷于“猜测”您何时需要分号。在这种情况下,JS“看”这段代码是

function avg(a, b) {
    return;
        (a + b) / 2;
}
console.log(avg(2, 3));

但是,如果您这样写:

function avg(a, b) {
    return (a
          + b) / 2
}
console.log(avg(2, 3))

那么,结果将是正确的。在这种情况下,如果 JS 尝试在 (a 之后插入分号,则会导致无效语法,因为 ( 需要闭合。

JS 中的变量提升和作用域

与 C、C++ 或 Java 不同,JS 只有两种简单的作用域类型——全局和函数。因此,在 JS 中,forifwhile 等不会定义作用域块。因此,像这样的代码

function () {
    if (someCondition) {
        var a;
    }
}

实际上被视为

function () {
    var a;
    if (someCondition) {
        // ...
    }
}

JS 的这种行为也称为变量提升(hoisting)。就像升起旗帜一样,它将所有变量声明提升到该作用域的顶行。

再举一个例子

function test() {
    'use strict';
    console.log(a);
    console.log(b);
    //console.log(x);
    var a = 10, b = 10;
    console.log(a);
    console.log(b);
}
test();

这是一个独特的例子。在这种情况下,输出将是

undefined
undefined
10
10

但是,如果您取消注释 console.log(x) 这一行,您将遇到错误——ReferenceError: x is not defined。这是因为 ab 被提升到函数顶部,因此在 console.log 语句运行时它们存在,但它们尚未被赋值。

function test() {
    'use strict';
    console.log(a);
    console.log(b);
    var a = b = 10;
    console.log(a);
    console.log(b);
}
test();

注意 var 行。在这种情况下,不仅 console.log(b) 行会报错,而且 var 行也会报错,前提是外部作用域中尚未定义变量 b。这是因为在这种情况下,b 是表达式的一部分,所以 var 不定义 b,只定义 a

所有在函数块之外定义的变量(无论是在 JS 文件中还是在 <script> 块中)都处于全局作用域。由于只有一个全局作用域,所以它们都可以在任何地方访问。

关于函数

函数也是对象

我将在本指南中反复强调这一点。这很重要。函数是 Function 类型的对象。像任何其他对象一样,它们也有方法!而且像任何对象一样,它们也可以在任何地方定义,从其他函数返回,或作为参数传递等等。

以下面的例子为例

function gen() {
    return function ans(factor) {
        return 2 * factor;
    };
}

看起来很 confusing 吗?如果是,那么我们用一个变量来代替返回的函数。

function gen() {
    // ...
    return f;
}

看起来好多了吗?由于函数只是普通对象,我们可以执行以下任何操作

function gen() {
    var f = function ans(factor) {
        return 2 * factor;
    };
    return f;
}

或者

function gen() {
    function ans(factor) {
        return 2 * factor;
    };
    return ans;
}

赋值类比

当你命名一个函数(以函数 f 为例),就像

function f(factor) {
    return 2 * factor;
}

那几乎等同于

var f = function (factor) {
    return 2 * factor;
};

我说“几乎”,因为尽管……

f(2);
var f = function (factor) {
    return 2 * factor;
};

...会报错,说 TypeError: f is not a function,因为 f 实际上是 undefined。但是,...

f(2);
function f(factor) {
    return 2 * factor;
}

...不会出错。因为,与 var 类似,函数定义也会被提升。

函数不是多态的

你不能命名两个同名的函数,并希望根据参数类型来调用它们中的任何一个。后定义的函数会覆盖之前的函数。毕竟,如果你采用赋值的类比,那么下一个函数定义将用它自己的 function 对象重新赋值给同一个变量。

function ans(f1, f2) { ... }

function ans(f1) { ... } // This replaces the previous definition.

但需要注意的是,函数中的所有参数始终是可选的。

function ans(a, b) {
    //...
}
ans(2); //ans will be invoked with a = 2, and b = undefined

函数返回

在一个函数中,您可以选择返回任何数据,或者什么也不返回。

function () {
    if (cond1) {
        // Returns an object
        return {
            a: 10
        };
    } else if (cond2) {
        // Returns undefined
        return;
    } else if (cond3) {
        // Returns a number.
        return 1;
    }
}

如果所有条件都失败了怎么办?类似于 cond2,这将返回 undefined

闭包

JS 拥有 Lambda 的强大功能。简而言之,Lambda 是匿名函数。这已被证明是该语言的核心支柱之一。现在,它甚至被引入到 Java 8 中。

JS 中的所有函数都可以访问外部作用域,无论是另一个函数还是全局作用域。即使外部函数执行完毕,它也能够保留外部作用域。这种保留外部作用域的概念就是闭包

Java 开发者会熟悉 final 的概念。匿名内部类可以访问外部作用域中的 final 变量并保留它。这类似于闭包,但不是 100%,因为闭包要求捕获整个外部作用域。尽管 JS 解释器优化了它们的实现,并且只保留实际引用的变量。此外,在真正的闭包中,您被允许更新外部作用域中变量的值。

有了这些知识,你能猜出下面代码的输出吗?

function adder(factor) {
    return function (a) {
        return a + factor;
    };
}
var adder2 = adder(2);
console.log( adder2(5) );

如果您猜对了 7,那就是正确的。adder2 变量引用了一个由 adder 生成的函数,它总是将 2 加到任何传递给它的数字上。

如果你觉得难以理解,那么 adder2 实际上是这样

adder2 = function (a) {
    return a + 2;
};

现在猜猜下面的。

function gen() {
    var i = 1, f = [];
    for (; i <= 2; i++) {
        f.push(function (a) { return a + i; });
    }
    return f; // Returns an array with two functions in it.
}
var fs = gen();
console.log( fs[0](5) );
console.log( fs[1](5) );

如果您的答案不是 88,那么就错了!fs[0]fs[1] 返回在 gen 的 for 循环中生成的函数。请记住,在这种情况下,这两个函数都保留了相同的外部作用域,而不是 i 的值。当 for 循环结束时,i 的值为 3。因此,这两个函数都将 3 添加到 5 中,而不是将 12 添加到 5 中。

真值与假值

与 C 和 C++ 相似,但与 Java 不同,JS 有广泛的真值(truthy)和假值(falsy)概念。所有对象(空字符串除外)和非零数字都被视为真值。而空字符串、零、nullundefined 则被视为假值。

undefined 是一个特殊值。所有未被赋值的变量都具有 undefined 值。清楚吗?:) 同样,所有不返回值的函数实际上都返回 undefined。事实上,它是一个关键字。因此,以下代码是有效的

var a = undefined;

这实际上等同于

var a;

值强制转换

在 JS 中,当您尝试对值执行一些不可能的操作时,JS 会尽力使它们兼容并得出一些有意义的结果。

例如:!0 实际上是布尔值 true,因为 ! 只能与布尔值一起使用。0 被强制转换为布尔值时是 false'2' * 1 实际上是数字 2,因为 * 无法作用于字符串。但是,'2' + 1 是字符串 21,因为由于存在一个 string,数字被强制转换为 string

这里有一个提示。由于 !0true。你可以用它来做一个巧妙的技巧——var hasChildren = !!children.length;。这会将 hasChildren 设置为适当的纯布尔值。

基于原型的编程

与 C、C++ 或 Java 不同,JS 中的函数实际上是对象,正如 OOP 开发者所说,是 Function 类的一个实例。然而,JS 中没有类,只有构造函数。构造函数通过克隆另一个对象来创建对象。因此,JS 中的所有函数都是 Function 的克隆。只有函数才能作为构造函数,即 new 运算符只能应用于它们。

用 Douglas Crockford 的话说:你创建原型对象,然后……创建新的实例。JavaScript 中的对象是可变的,因此我们可以增强新的实例,赋予它们新的字段和方法。这些又可以作为更新的对象的原型。我们不需要类来创建大量相似的对象……对象继承自对象。还有什么比这更面向对象的呢?

JS 支持两种对象创建方式——通过克隆现有对象(使用 Object.create(otherObj))或从无到有("from nothing",使用 Object.create(null))。顺便说一句,{}Object.create(Object.prototype) 的简写,而 []new Array() 的简写。

实际上,Object.create(obj) 创建了一个新对象(可以把它想象成一个空壳),其中 obj 是它的原型(这给那个空壳提供了内容)。所以,它实际上并没有克隆 obj;相反,它将 obj 设置为它的原型。顾名思义,原型是一个对象,主对象从中派生其属性和方法。但是,您也可以直接向主对象添加任何属性或方法。

Object.prototype 本身是一个从无到有的对象,其他对象都继承它,包括 Function.prototype。对象中的 prototype 属性本身也是一个对象,并且可以拥有其他原型,形成一个链。稍后会详细介绍。获取对象原型的标准方法是使用 Object.getPrototypeOf(obj)。但是,IE8 及以下版本没有实现这个方法。非标准方法(Internet Explorer 也不支持)是使用 __proto__ 属性。对于 Internet Explorer 和其他浏览器,您可以使用 obj.constructor.prototype

new 运算符

你可以猜到。与 Java 类似,new Foo() 将创建一个 Foo 类型的新对象。当我说它是 Foo 类型时,这意味着该对象的原型设置为 Foo.prototype。你会记得,你也可以使用 Object.create() 来做同样的事情。所以,new Foo() 几乎等同于 Object.create(Foo.prototype)。我说“几乎”,因为在前一种情况下,Foo 函数在返回创建的对象之前执行。在后一种情况下,Foo 函数不执行。

这是什么?

这是新 JS 开发者最主要的困惑点之一。在 JS 中,函数总是在某个上下文(隐式或显式)中执行。该上下文决定了函数内部 this 的值。同一个函数可以用任何显式上下文调用。当未指定上下文时,非严格模式下为“window”,严格模式下为 undefined。您可以使用以下代码进行测试

function A() { return this; }
A(); // returns window
function B() {'use strict'; return this; }
B(); // returns undefined

看下面

var car = new Car();
car.honk('loud');

当您使用 new 运算符时,您创建了一个 Car 类型的新对象。当您执行 car.honk('loud') 时,JS 解释器首先在 car 对象中查找 honk 方法,如果未找到,则接下来会在 Car.prototype 对象中查找该方法。如果该方法甚至不存在,它将继续在 Car.prototype.prototype 对象中查找,以此类推。一旦找到该方法,该方法将在 car 对象的上下文中触发。这意味着,在该方法中,this 将是 car。这些行为是 JS 语言的一部分。

回想一下,函数本身就是 Function 类型的对象,这意味着它们也有方法,并且反过来可以作为对象使用!函数有一个 call 方法,您可以使用它明确指定函数执行的上下文。

Car.prototype.honk.call(someObj, 'loud');

这将调用 honk,使得其中的 this 指向 someObj。实际上,someObj 可以是任何对象,不一定是 Car 类型的对象。

Function 类中还有一个 apply() 方法。它与 call() 的唯一区别在于,这里第二个参数是我们需要发送给被调用函数的参数数组。

在下一节中,我们将把这些信息付诸实践。

原型继承与面向对象编程截然不同

在基于类的继承中,编译器负责自动为您实现继承。然而,在原型继承(JS)中,这留给了开发人员自行处理。实际上,原型继承是开发人员开发的一种概念/技巧,而不是语言定义的东西。

我们期望继承的主要特性是能够从父类继承方法和字段,并且如果需要,我们应该能够覆盖它们。

现在让我们尝试在 JS 中模仿这种行为。

function Vehicle(color) {
    this.color = color;
}
Vehicle.prototype.honk = function() {
    alert('Honking!');
};

function Car(color) {
    Vehicle.call(this, color);
}
Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.getWheelsCount = function() {
    return 4;
};

function Autorickshaw(color) { // OR TukTuk, take your pick
    Vehicle.call(this, color);
}
Autorickshaw.prototype = Object.create(Vehicle.prototype);
Autorickshaw.prototype.getWheelsCount = function() {
    return 3;
};

上面,Vehicle.call(this, color) 语句在当前对象的上下文中执行 Vehicle 函数,并传递 color 参数。这样,我们实际上做了一个 super() 调用。所以,this.color 就像一个字段变量,而 this.honk()this.getWheelsCount() 则是方法。

在这种情况下形成的原型链是

Car.prototype -> Vehicle.prototype

现在,上面有很多样板代码。让我们尝试削减一下。

function define(superClass, definition) {
    function Class() {
        if (superClass)
            superClass.apply(this, arguments);
        if (definition.initialize)
            definition.initialize.apply(this, arguments);
    }
    if (superClass)
        Class.prototype = Object.create(superClass.prototype);
    var proto = Class.prototype;
    for (var key in definition) {
        proto[key] = definition[key];
    }
    return Class;
}

var Vehicle = define(null, {
    initialize: function(color) {
        this.color = color;
    },

    honk: function() {
        alert('Honking!');
    }
});

var Car = define(Vehicle, {
    getWheelsCount: function() {
        return 4;
    }
});

var Autorickshaw = define(Vehicle, {
    getWheelsCount: function() {
        return 3;
    }
});

define 方法相当直观。不过,在我继续之前,请注意arguments关键字。这个神奇的变量在函数内部可用。它是一个“数组”,包含在函数被调用时提供给该函数的所有参数。我说“数组”是带引号的,因为这实际上不是一个标准的 JS 数组。它只有数组的少数特性和方法。

该函数内部定义了另一个函数,即我们正在定义的新类。需要快速注意的是,该函数的名称是 Class。这意味着要定义它的实例,我们应该写 new Class()。然而,我们写在 new 运算符旁边的名称并没有什么意义。该名称只是对动作函数对象的引用。因此,如果 A = B = Class,那么 new A()new B()new Class() 都将产生相同的结果。

然后函数遍历提供的单例对象,并简单地将它们以相同的键复制到 Classprototype 中。最后,它返回该函数——Class。细心的读者会注意到 define 感觉类似于 PrototypeJs 的 Object.extends()

现在我们为 define 添加一些功能。

function define(superClass, definition) {
    function Class() {
        if (superClass) {
            this.$super = superClass.prototype; //New addition
            superClass.apply(this, arguments);
        }
        if (definition.initialize)
            definition.initialize.apply(this, arguments);
    }
    if (superClass)
        Class.prototype = Object.create(superClass.prototype);
    var proto = Class.prototype;
    for (var key in definition) {
        proto[key] = definition[key];
    }
    return Class;
}

我们刚刚添加了一个 this.$super,我们可以用它来访问超类方法,就像在其他语言中一样。

var Vehicle = define(null, {
    initialize: function(color) {
        this.color = color;
    },

    honk: function() {
        alert('Honking!');
    }
});

var Car = define(Vehicle, {
    honk: function() {
        this.$super.honk(); // This will display the Honking! alert.
        alert('Beep Beep');
    }
});

您可能会问,如何模拟 private 方法?好吧,我们根本不模拟。我们用 _ 前缀方法名称,表示它是 private。约定比强制规则更简单。但是,如果您真的想强制执行,那么还有另一种定义类的方法。

function Vehicle(color) {
    var that = this;
    this.color = color;

    function _showColor() { // This is a private method
        alert(that.color);
    }

    this.honk = function() {
        _showColor();
        alert('Honking!');
    };
}

这将产生与我们目前所用的类似的效果,并增加了 private 方法的好处。请注意,我们定义了一个变量 that。这是必需的,以便 _showColor() 可以引用它。它不能简单地使用 this,因为 this 具有特殊的含义。

在这种方法中,我们利用了闭包的强大功能。但是,您会注意到这种方法不如前一种方法性能好,因为每个 Vehicle 类型的对象都会创建一个新的函数实例。让我们看看 Car 如何继承它。

function Car(color) {
    Vehicle.call(this, color);
    this.getWheelsCount = function () {
        return 4;
    };
}
Car.prototype = new Vehicle();

这是关键区别。此类型 Car 的原型不是 Vehicle.prototype,而是 Vehicle 的对象。

在这种情况下形成的原型链是

Car.prototype -> new Vehicle() -> Vehicle.prototype

还有另一种定义类的方法。

function Vehicle(color) {
    this.color = color;
}
Vehicle.prototype = {
    honk: function() {
        alert('Honking!');
    }
};

在这里,我们将默认的原型对象替换为另一个对象。没有改变什么,但这更方便阅读和输入。

ECMAScript 6 有一个提案来支持 classextends 关键字。最终,在未来,我们可能会在 JS 中获得实际的类支持。

instanceof 运算符

Java 开发者会立即识别出这一点。与 Java 中一样,如果左侧的对象是给定类类型(右侧给定)的,则此运算符的计算结果为 true。其语法是

object instanceof function

这将使 JS 沿着 object 的原型链查找 function.prototype。所以,

console.log(car instanceof Car); //Is true
console.log(car instanceof Vehicle); //Is true
console.log(car instanceof Autorickshaw); //Is false

但是,

var o = Object.create(null);
console.log(o instanceof Object); // Is false

这是假的,因为 o 是一个从无到有的对象,它不是任何东西的对象。所以你可以说,它只是一个空壳。它也可以用作 map,类似于 {},它是一个 Object 类型的对象。

浏览器中的 JS 是单线程的

JS 语言并没有规定解释器必须是单线程的,事实上,许多服务器端解释器,如 nodeJs,都不是。然而,浏览器解释器是单线程的。(现代浏览器现在支持 Web Workers API,它可以启动后台线程。)

这是一个需要牢记的重要事实。因此,无论 Ajax 调用何时完成,您提供的回调都不会触发,除非您完成了当前正在做的事情。

另请注意,当 JS 正在处理时,浏览器会锁定。它无法立即响应用户输入。因此,如果您有长时间运行的 JS 任务,那么浏览器可能会停止响应。如果您使用的是现代浏览器,请使用 Web Workers,或者将任务分成更小的块,并定期调用 setTimeout(),以便在继续下一个任务块之前将控制权返回给浏览器。我有一个项目 CInk,它执行一些繁重的渲染任务;我就是用这个技巧的。

DOM 不是 JS

我们通常在浏览器 JS 中看到的魔法对象,例如 windowdocument 等,都是由浏览器定义的 DOM 对象。DOM 代表文档对象模型。这是一种用于表示 HTML 代码的树数据模型。这些对象由浏览器注入 JS 领域,并且它们不是由 JS 语言规范定义的。DOM 有自己的规范——文档对象模型(DOM)级别 1、2 和 3。该规范是在 ECMAScript 标准化之后1形成的。

不幸的是,DOM 有一个广泛的 API 列表,这些 API 因浏览器而异。简短列表,您可以查看 QuirksMode 上的 DOM Core

然而,我在这里不是要谈论什么是 DOM。我只想强调 DOM 和 JS 是相互独立的东西。因此,在服务器端 JS 编码时,不要期望这些 DOM 对象。您可能会在那里找到其他神奇的全局对象。

总结

希望这有所帮助,祝您编程愉快。:)

© . All rights reserved.