JavaScript 重要概念速览
JavaScript 重要概念速览
引言
在本指南中,我试图将 JavaScript 的所有重要核心概念汇集在一起。学习 JS 对我来说非常困难,因为所有这些部分都散落在网络上。我多年来逐渐找到了它们。我希望我已涵盖了所有内容。如果您觉得我遗漏了重要内容或有任何不正确之处,请在评论中告诉我。
无论标题如何,本指南都应有助于所有开发人员。但是,在阅读本指南之前,您应该熟悉 JS 的基础知识。这更像是 JS 概念的集合。
松散类型
没错!无需绞尽脑汁决定这应该是 float
还是 double
,int
还是 short
或 long
甚至是 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 中,for
、if
、while
等不会定义作用域块。因此,像这样的代码
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
。这是因为 a
和 b
被提升到函数顶部,因此在 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) );
如果您的答案不是 8
和 8
,那么就错了!fs[0]
和 fs[1]
返回在 gen
的 for 循环中生成的函数。请记住,在这种情况下,这两个函数都保留了相同的外部作用域,而不是 i
的值。当 for
循环结束时,i
的值为 3
。因此,这两个函数都将 3
添加到 5
中,而不是将 1
和 2
添加到 5
中。
真值与假值
与 C 和 C++ 相似,但与 Java 不同,JS 有广泛的真值(truthy)和假值(falsy)概念。所有对象(空字符串除外)和非零数字都被视为真值。而空字符串、零、null
和 undefined
则被视为假值。
undefined
是一个特殊值。所有未被赋值的变量都具有 undefined
值。清楚吗? 同样,所有不返回值的函数实际上都返回
undefined
。事实上,它是一个关键字。因此,以下代码是有效的
var a = undefined;
这实际上等同于
var a;
值强制转换
在 JS 中,当您尝试对值执行一些不可能的操作时,JS 会尽力使它们兼容并得出一些有意义的结果。
例如:!0
实际上是布尔值 true
,因为 !
只能与布尔值一起使用。0
被强制转换为布尔值时是 false
。'2' * 1
实际上是数字 2
,因为 *
无法作用于字符串。但是,'2' + 1
是字符串 21
,因为由于存在一个 string
,数字被强制转换为 string
。
这里有一个提示。由于 !0
是 true
。你可以用它来做一个巧妙的技巧——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()
都将产生相同的结果。
然后函数遍历提供的单例对象,并简单地将它们以相同的键复制到 Class
的 prototype
中。最后,它返回该函数——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 有一个提案来支持 class
和 extends
关键字。最终,在未来,我们可能会在 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 中看到的魔法对象,例如 window
、document
等,都是由浏览器定义的 DOM 对象。DOM 代表文档对象模型。这是一种用于表示 HTML 代码的树数据模型。这些对象由浏览器注入 JS 领域,并且它们不是由 JS 语言规范定义的。DOM 有自己的规范——文档对象模型(DOM)级别 1、2 和 3。该规范是在 ECMAScript 标准化之后1形成的。
不幸的是,DOM 有一个广泛的 API 列表,这些 API 因浏览器而异。简短列表,您可以查看 QuirksMode 上的 DOM Core。
然而,我在这里不是要谈论什么是 DOM。我只想强调 DOM 和 JS 是相互独立的东西。因此,在服务器端 JS 编码时,不要期望这些 DOM 对象。您可能会在那里找到其他神奇的全局对象。
总结
希望这有所帮助,祝您编程愉快。