Refresh your JavaScript Knowledge
JavaScript 知识巩固
诞生
JavaScript 于 1995 年被网景的一名工程师 Brendan Eich 所创造
第一次发布于 1996 年早期的 Netscape 2
最初叫 LiveScript
但由于不幸的营销决策,为了利用 Java 的热度,而改名为 JavaScript(但与 Java 毫无联系)
发展历程
几个月后,微软在 IE 3 中发布了 JScript
几个月后,网景向 ECMA(欧洲标准组织)国际组织提交了 JavaScript
于是在那一年诞生了 ECMAScript 标准的第一个版本
这个标准在 1999 年收到了一个具有象征意义的更新,定为 ECMAScript 第三版本,从那以后版本趋于稳定
由于有关语言复杂性的政治分歧,第四个版本被废弃掉
随后第四版本的许多部分成为了第五个版本的基础,发版于 2009 年 12 月
第六版发布于 2015 年 6 月
应用场景
不像大多数语言,JavaScript 没有输入和输出的概念
它被设计用来在宿主环境作为一种脚本语言运行,并且由宿主环境决定其与外部世界沟通的机制
最普遍的宿主环境是浏览器
但 JavaScript 解释器还能在以下地方找到:
- Adobe Acrobat
- Adobe Photoshop
- SVG images(矢量图)
- Yahoo’s Widget engine(雅虎组件引擎)
- 服务端环境,例如 Node.js
- 非关系型数据库,例如开源的 Apache CouchDB
- 嵌入式计算机
- 复杂的桌面环境,例如 GNOME(GNU/Linux 操作系统最著名的图形界面系统之一)
- 其他
概述
JavaScript 是一种多范式、动态语言,拥有类型、操作符、标准内建对象和方法
它的语法基于 Java 和 C(许多这二者语言的结构都被应用于 JavaScript)
JavaScript 支持面向对象编程通过使用对象原型(object prototypes)取代类(更多见**原型继承和 ES2015类**概念)
JavaScript 也支持函数式编程——函数是对象,给予函数容纳可执行代码的能力并将像其他对象一样进行传递
JavaScript 类型
- Number
- String
- Boolean
- Function
- Object
- Symbol(ES2015 新加)
技术上更严谨的分类如下:
Number
String
Boolean
Symbol(ES2015 新加)
Object
- Function
- Array
- Date
- RegExp
- Math
null
undefined
还有一些内建的 Error 类型
Number
JavaScript 的 Numbers 是 double-precision 64-bit format IEEE 754 values
由于这个特点,JavaScript 中没有整型,所以在 C 或 Java 中使用到的算数运算要在 JavaScript 中留意
比如:
0.1+0.2
0.30000000000000004
实践中,整型值会被当做 32 位整数,并且甚至有些实现以这种方式存储,直到被要求去执行一条在 Number 上有效但在 32 位整型上无效的指令,这对于位运算来说很重要
原文:In practice, integer values are treated as 32-bit ints, and some implementations even store it that way until they are asked to perform an instruction that’s valid on a Number but not on a 32-bit integer. This can be important for bit-wise operations.
标准的算数运算符被支持,包括加、减、取模、取余等等
内建对象 Math 提供了高级数学运算函数和常量
Math.sin(3.5);
-0.35078322768961984
r = 2;
var circumference = 2 * Math.PI * r;
console.log(circumference)
使用内建函数 parseInt()可以将一个字符串转换为整型数,但是要注意给该函数指定第二个参数(要转换的进制),如果不填会得到意想不到的结果
parseInt('010')
10
parseInt('0x10')
16
parseInt('010', 8)
8
parseInt('0x10', 16)
16
parseInt('11',2)
3
转换为八进制省略第二个参数的方式在 2013 年后被废除,但十六进制忽略第二个参数的用法仍存在,因为可以识别到十六进制前缀0x
还有内建函数 parseFloat 用于将字符串转换为浮点数,但不同于 parseInt(),它总是默认以十进制方式转换
另外,还可以通过一元运算符+
将值转换为数值
parseFloat('12.34')
12.34
+ '56.78';
56.78
+ '0x10'
16
+ '42'
42
如果字符串是非数值,转换会返回一个特殊值 NaN(Not a Number)
parseInt('hello', 10)
NaN
如果将 NaN 作为输入,做任何算数运算所得都是 NaN
parseInt('hello', 10) + 5
NaN
可通过内建函数 isNaN()判定是否为 NaN
isNaN(parseInt('hello', 10) + 5)
true
JavaScript 还提供了特殊值:Infinity 和-Infinity
1 / 0;
Infinity
-1 / 0;
-Infinity
可以使用内建函数 isFinite()判断 Infinity、-Infinity 和 NaN
isFinite(1 / 0);
false
isFinite(-1 / 0);
false
isFinite(NaN);
false
parseInt()、parseFloat()和
+
的区别:前两者会将字符串转换,直到遇到不是有效的数字止,而+
会直接将字符串转换为 NaN 如果字符串内包含无效字符
parseInt('10.2abc');
10
+ '10.2abc'
NaN
String
JavaScript 中的字符串是 Unicode 字符序列,这对于处理国家化问题来说很方便,更准确地讲,是 sequences of UTF-16 code units,每一个码单元通过一个 16 位数字呈现,每一个 Unicode 字符通过 1 个或 2 个码单元呈现
如果想呈现一个单字符,只需要使用一个包含单个字符的字符串
如果想知道一个(码单元中的)字符串的长度,访问其 length 属性
'hello'.length
5
字符串也可当做对象,并通过方法来操作字符串的信息
'hello'.charAt(0);
"h"
'hello, world'.replace('hello', 'hola');
"hola, world"
'hello'.toUpperCase();
"HELLO"
其他类型
JavaScript 用 null 表示 non-value(并且也仅能通过null
访问)
还有 undefined 表示一个尚未初始化的值(表示一个还未被赋值的变量)
如果你声明一个变量,但没对其赋值(assign),这个变量的类型就是 undefined,但 undefined 实际上是一个常量
JavaScript 还有一个布尔类型,只有两个值,true 和 false
任何值都能被转换成一个布尔值通过以下方式:
false、0、空字符串("")、NaN、null 和 undefined
所以其他都被判定为布尔中的 true
使用 Boolean()函数来具体实现
Boolean('');
false
Boolean(234);
true
这很少情况需要这样处理,当 JavaScript 期望一个布尔值时,会静默执行布尔转换,例如if
statement
布尔操作符,例如&&(逻辑与)、||(逻辑或)和!(逻辑非)都被支持
变量
JavaScript 通过三个关键字声明新的变量:let、const 和 var
let 允许声明块级变量,其声明的变量仅在封闭块中有效
for (let i = 1; i < 5; i++) {
console.log(i);
}
console.log(i);
VM959:2 1
VM959:2 2
VM959:2 3
VM959:2 4
VM959:4 Uncaught ReferenceError: i is not defined
at <anonymous>:4:13
const 允许声明永远不会企图改变的变量,并且也仅在所声明的块范围内有效
for (const j = 2; j < 4; j++) {
console.log(j);
}
console.log(j);
VM1175:2 2
VM1175:1 Uncaught TypeError: Assignment to constant variable.
at <anonymous>:1:27
var 是最通用的声明关键字,它没有 let 和 const 的限制
它是传统 JavaScript 唯一的声明变量的关键字
for (var k = 3; k < 7; k ++) {
console.log(k);
}
console.log(k);
VM1257:2 3
VM1257:2 4
VM1257:2 5
VM1257:2 6
VM1257:4 7
JavaScript 和其他语言(比如 Java)很重要的一个区别是代码块并没有域,只有函数才有域
所以如果在一个 compound statement(例如在 if 控制结构范围内)内使用 var 声明的变量在整个函数范围内都可见,如上例
然而,从 ES2015 开始,let 和 const 的声明允许创建块级域变量
运算符
JavaScript 数值运算符有+
、-
、*
、/
和%
通过=
赋值
还有一些复合赋值操作,例如+=
和-=
,这相当于 x = x + y 或 x = x - y
你可以使用++
和--
分别表示递增和递减,这些都可以被用作运算符前缀或后缀
+
运算符还可用作字符串连接符
'hello' + 'world';
"helloworld"
如果你把一个字符串追加于一个数字或其他值,都会首先被转化为一个字符串
"3" + 4 + 5;
("345");
3 + 4 + "5";
("75");
为某个值追加一个空字符串是一个将其转换为字符串的方式
JavaScript 使用<
、>
、<=
和>=
进行比较操作
这些既对字符串有效,也对数字有效
判断两个值相等并不是那么直接,如果给双等运算符==
两个不同类型的值,会表现出类型约制
123 == "123";
true;
1 == true;
true;
为避免约制,使用三等运算符
123 === "123";
false;
1 === true;
false;
还有!=和!==
JavaScript 还有**位运算符**
控制结构
JavaScript 有一套类似 C 语言家族的控制结构
条件语句通过 if 和 else 支持
var name = "hello";
if (name == "test") {
name += "test";
} else if (name == "hello") {
name += "world";
} else {
name += "!";
}
name == "helloworld";
true;
JavaScript 拥有 while 循环和 do-while 循环,前者用于基本循环,后者用于你想确保循环至少执行一次
while (true) {
// an infinite loop!
}
var input;
do {
input = get_input();
} while (inputIsNotValid(input));
JavaScript 的 for 循环和 C 还有 Java 的一样,使你能够在一行内提供控制信息
for (var i = 0; i < 5; i++) {
// Will execute 5 times
}
JavaScript 还有两个高级 for 循环
- for of
for (let value of array) {
// do something with value
}
- for in
for (let property in object) {
// do something with object property
}
&&和||运算符有短路逻辑,意味着第二个运算值的是否执行决定于第一个运算值
这有助于检查空对象在访问其属性之前
o = null;
var name = o && o.getName();
null;
或进行缓存值(当假值无效时)??
var name = cachedName || (cachedName = getName());
JavaScript 对条件语句拥有一个三元运算符
age = 19;
var allowed = (age > 18) ? 'yes' : 'no';
console.log(allowed);
VM1682:3 yes
switch
语句可被用于基于一个数字或字符串的多分支判断
switch (action) {
case "draw":
drawIt();
break;
case "eat":
eatIt();
break;
default:
doNothing();
}
如果你没有添加 break 语句,将会在该条件下的内容执行后继续向下执行,这可能并不是你想要的,如果你的确想这么做用于调试,请添加注释表明
switch (a) {
case 1: // fallthrough
case 2:
eatIt();
break;
default:
doNothing();
}
默认条款是可选的,你在 switch 部分和 case 部分都可以有表达式,比较会在二者使用了===运算符时发生
switch (1 + 3) {
case 2 + 2:
console.log('execute 4');
break;
default:
neverhappens();
}
VM1831:3 execute 4
对象
JavaScript 的对象可以理解为一个键值对集合,类似于:
Python 中的字典
Perl 和 Ruby 中的 Hashes
C 和 C++中的哈希表
Java 中的 HashMap
PHP 中的关联数组
事实上这个数据结构被如此广泛的使用,是其多才多艺的一个佐证
因为 JavaScript 中的一切都是对象
任何 JavaScript 程序自然包含着一个强大的哈希表查找,这是个好事,因为很快
JavaScript 对象的“键”部分是一个字符串,“值”部分可以是任何值
这允许你可以构造任意复杂的数据结构
有两种基本的创建对象的方法:
var obj = new Object();
和
var obj = {};
这二者语义上相等,后者称为 object literal syntax,并且更方便
这种语法也是 JSON 格式的核心并总被偏爱
文字对象语法可以用来完整初始化一个对象:
var obj = {
name: 'test',
_for: 'max',
details: {
color: 'orange',
size: 12
}
};
obj
{name: "test", _for: "max", details: {…}}
属性可被链接到一起
obj.details.color;
("orange");
obj["details"]["color"];
("orange");
下面的例子创建了一个对象原型 Person 和一个原型实例 You
function Person(name, age) {
this.name = name;
this.age = age;
}
var you = new Person('You', 24);
console.log(you);
VM2069:7 Person {name: "You", age: 24}
一经创建,一个对象的属性可被再次访问用以下两种方式:
obj.name = "magi";
var name = obj.name;
("magi");
obj["name"] = "igma";
var name = obj["name"];
("igma");
var user = prompt("what is your key?");
obj[user] = prompt("what is your value?");
("111");
这些也语义上相等,后者优势在于 name 属性作为一个字符串被提供,意味着可以在运行时被计算
然而,使用这种方式可以防止了有些 JavaScript 引擎和优化器被应用
另外,也因此可以使用关键字来设置和获得属性
obj.for = "Simon"; // Syntax error, because 'for' is a reserved word
obj["for"] = "Simon"; // works fine
从 ECMAScript 5 开始,保留字可以用作对象属性名 in the buff。这意味着定义对象时不再需要引号来包裹,详情见the ES5 Spec
更多关于对象和原型,见Object.prototype
关于对象原型和对象原型链,见继承和原型链
从 ECMAScript2015 开始,对象的键可以被使用括号符的变量定义
var userPhone = {['phoneType']: 12345};
console.log(userPhone);
VM2470:2 {phoneType: 12345}
可以代替
var userPhone = {};
userPhone['phoneType'] = 12345;
console.log(userPhone);
VM2505:3 {phoneType: 12345}
数组
JavaScript 中的数组其实一种特殊类型的对象
和常规的对象非常像(数值属性只能使用[]语法访问)
但有一个神奇的属性叫做“length”
其总是比数组最大索引值多一位
创建数组的方式如下:
var a = new Array();
a[0] = "dog";
a[1] = "cat";
a[2] = "hen";
a.length;
3;
一个更方便的表示方式是使用数组文字(array literal)
var a = ["dog", "cat", "hen"];
a.length;
3;
注意 array.length 不必是数组项目的个数,考虑下面一种情况
var a = ["dog", "cat", "hen"];
a[100] = "fox";
a.length;
101;
谨记:数组的长度总比数组索引最大值大一位
如果访问一个不存在的数组索引,会得到一个值为 undefined 的返回值
typeof a[90];
("undefined");
如果把上面的[]和 length 纳入考虑,你可以使用 for 循环迭代一个数组
for (var i = 0; i < a.length; i++) {
console.log(a[i]);
}
VM302:2 dog
VM302:2 cat
VM302:2 hen
97VM302:2 undefined
VM302:2 fox
ECMAScript 介绍了一种更加简明的循环 for of,用来迭代对象,比如数组:
for (const currentValue of a) {
console.log(currentValue);
}
VM394:2 dog
VM394:2 cat
VM394:2 hen
97VM394:2 undefined
VM394:2 fox
你也可以使用 for in 循环来迭代一个数组,但如果有人向 Array.prototype 添加了新的属性,它(新加的属性)在本次循环也会被迭代。所以这种循环类型不被推荐用于数组迭代
另一种在 ECMAScript5 中添加的用来迭代数组的方式是 forEach()
['dog', 'cat', 'hen'].forEach(function(currentValue, index, array) {
console.log('currentIndex: ' + index + ' currentValue: ' + currentValue + ' array: ' + array);
});
VM486:2 currentIndex: 0 currentValue: dog array: dog,cat,hen
VM486:2 currentIndex: 1 currentValue: cat array: dog,cat,hen
VM486:2 currentIndex: 2 currentValue: hen array: dog,cat,hen
如果想要为一个数组追加元素,只需要简单地:
a.push(item);
数组方法列表如下:
Method name | Description | Example |
---|---|---|
a.toString | 返回数组的每一个元素以逗号分隔的字符串 | dog,cat,hen,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,fox |
a.toLocaleString() | 同 toString,不过先判断指定语言环境,没指定则使用默认语言环境,主要用于 Date 类型 | - |
a.concat(item1[, item2[, …[,itemN]]]) | 返回一个追加于其后的一个新的数组 | a.concat(‘panda’,‘seal’)-> [empty × 97, “hen”, “cat”, “dog”, “panda”, “seal”] |
a.join(sep) | 转换数组为一个字符串,伴随着以 sep 参数分隔的值 | dog-cat-hen————————————————————————————————–fox |
a.pop | 移除并返回最后一项 | “fox” |
a.push(item1, …, itemN) | 向数组末端追加元素,并返回追加后的数组长度 | 101 |
a.reverse() | 倒置数组 | (101) [“fox”, empty × 97, “hen”, “cat”, “dog”] |
a.shift | 移除并返回第一个元素 | “fox” |
a.slice(start[, end]) | 返回子数组 | a.slice(98,100) -> [“cat”, “dog”] |
a.sort([cmpfn]) | 采用可选比较函数进行排序 | a.sort()->[“cat”, “dog”, “hen”, empty × 97] |
a.splice(start, delcount[, item1[, …[, itemN]]]) | 让你修改一个数组,通过删除一部分并使用更多条目替换之 | a.splice(0, 2, ‘another cat’, ‘another dog’)->[“cat”, “dog”]->a->[“another cat”, “another dog”, “hen”, empty × 97] |
a.unshift(item1[, item2[, …[, itemN]]]) | 预先考虑数组开头的条目 | a.unshift(‘prepand item1’, ‘prepand item2’)->102->a->[“prepand item1”, “prepand item2”, “another cat”, “another dog”, “hen”, empty × 97] |
更多详见array methods & toLocaleString
函数
同对象一起,函数也是理解 JavaScript 的核心组件
最基本的函数不能再简单
function add(x, y) {
var total = x + y;
return tatal;
}
这演示了一个基本的函数
一个 JavaScript 函数可以有 0 个或更多的命名参数
函数体可以包含任意多你想要的语句并且可以声明它对于它本身,自己的变量
return 语句可以被用来在任何时候返回一个值,中断函数
如果没有返回语句(或者一个空的 return 而不包含任何值),JavaScript 会返回 undefined
命名参数比起其他任何事物更像是指导方针,只要你想,你可以调用一个函数而不用传递任何参数,这种情况它会传递 undefined 作为参数
所以直接运行上面定义的函数,会报错,因为你给函数传递了参数 undefined
function add(x, y) {
var total = x + y;
return total;
}
add();
NaN
你也可以传递函数期望的更多参数
function add(x, y) {
var total = x + y;
return total;
}
add(2, 3, 4);
5
参数“4”会被忽略
这可能会看起来有点蠢,但函数会访问其函数体内部的附加名为 arguments 的变量,它是一个类数组对象,承载了所有传递给函数的参数
让我们重写这个 add 函数来取到和我们想要的参数一样多的参数
function add() {
var sum = 0;
for (var i = 0, j = arguments.length; i < j; i++) {
sum += arguments[i];
}
return sum;
}
add(2, 3, 4, 5);
14
再写一个平均值函数
function avg() {
var sum = 0;
for (var i = 0, j = arguments.length; i < j; i++) {
sum += arguments[i];
}
return sum / arguments.length;
}
avg(2, 3, 4, 5);
3.5
这非常有用,但有一点啰嗦。要再减少一点这份代码,我们可以考虑参数数组作取代,通过Rest parameter syntax
用这种方式,我们可以保持代码最小化的同时传递任意数量的参数给函数
rest 参数运算符用于函数参数列表使用"…variable"格式,它会包含进调用函数时整个未捕获参数列表
我们也可以使用 for…of 循环取代 for 循环来返回变量中的值
function avg(...args) {
var sum = 0;
for (let value of args) {
sum += value;
}
return sum / args.length;
}
avg(2, 3, 4, 5);
3.5;
在上述的代码中,args 变量拥有我们传递进函数的所有函数
很重要需要注意无论何时 rest 参数运算符被放置在一个函数声明,它都会在它声明后存储所有的参数,但不会在声明之前(存储参数),例如:function avg(firstValue, …args)将存储被传递进函数的第一个值 在 firstValue 中,剩余参数存储在 args 中。另一个有用的函数但也的确给我们带来一个新问题。avg()函数接收一个逗号分隔的参数列表——但是要是你想要得到一个数组的平均值?你只能重写这个函数如下:
function avgArray(arr) {
var sum = 0;
for (var i = 0, j = arr.length; i < j; i++) {
sum += arr[i];
}
return sum / arr.length;
}
avgArray([2, 3, 4, 5]);
3.5;
但使得这个我们创建的函数可被重用会更好。
幸运的是,JavaScript 可以让你使用一个任意的参数数组来调用一个函数,通过使用任何函数对象的 apply()方法
avg.apply(null, [2, 3, 4, 5]);
3.5;
apply()的第二个参数是用作参数的数组;第一个参数后面再讨论,这强调了一个事实——函数也是对象
你可以在函数调用中使用 spread 操作符达到相同的结果,例如 avg(…numbers)
JavaScript 让你可以创建匿名函数
var arg = function() {
var sum = 0;
for (var i = 0, j = arguments.length; i < j; i++) {
sum += arguments[i];
}
return sum / arguments.length;
};
这在语义上等同于 function avg()形式
这非常强大,因为它可以让你把一个函数定义放在任何你通常放置表达式的地方
这使得所有种类的聪明的诀窍
这是一种“隐藏”一些本地变量的方式——像 C 语言中的块级域
var a = 1;
var b = 2;
(function() {
var b = 3;
a += b;
})();
console.log(a);
console.log(b);
VM2347:9 4
VM2347:10 2
JavaScript 允许你递归调用函数。
这对于处理树结构尤其有用,比如那些浏览器中的 DOM
function countChars(elm) {
if (elm.nodeType == 3) {
// TEXT_NODE
return elm.nodeValue.length;
}
var count = 0;
for (var i = 0, child; (child = elm.childNodes[i]); i++) {
count += countChars(child);
}
return count;
}
这强调了匿名函数的一个潜在问题:如果它没有名字,你怎么递归地调用它?
JavaScript 对于此让你可以给函数表达式命名
你可以使用命名了的 IIFEs(Immediately Invoked Function Expression)如下面所示:
var charsInBody = (function counter(elm) {
if (elm.nodeType == 3) { // TEXT_NODE
return elm.nodeValue.length;
}
var count = 0;
for (var i = 0, child; child = elm.childNodes[i]; i++) {
count += counter(child);
}
return count;
})(document.body);
undefined
charsInBody
58670
如上提供给一个函数表达式的名字仅对这个函数自己的域内可用
这允许更多的引擎优化并给出更多的可读代码
这个名称也出现在调试器和一些堆栈信息中,这会让你在调试时节省更多时间
注意 JavaScript 函数本身就是对象——像 JavaScript 中其他的一切一样——并且你可以添加或改变他们的属性,就像我们在对象部分所见过的
自定义对象
更多 JavaScript 面向对象编程见Object-Oriented JavaScript
在传统的面向对象语言编程中,对象是运算在数据上的数据和方法的集合
JavaScript 是基于原型的语言,没有像是在 C++或 Java 中的类语句(这有时会使得习惯于使用类语句的语言的编程者感到困惑)
取而代之,JavaScript 使用函数作为类
让我们考虑一个有名有姓的人作为对象
姓名可能会以两种方式展示:如“名 姓”或“姓 名”
使用我们前面讨论的函数和对象,我们就能展示数据如下:
function makePerson(first, last) {
return {
first: first,
last: last
};
}
function personFullName(person) {
return person.first + ' ' + person.last;
}
function personFullNameReversed(person) {
return person.last + ', ' + person.first;
}
s = makePerson('Simon', 'Willison');
console.log(personFullName(s));
console.log(personFullNameReversed(s));
VM1137:15 Simon Willison
VM1137:16 Willison, Simon
这有效,但很丑
这样最后你会在全局域有很多函数
我们真正需要的是一种把一个函数依附于一个对象的方式
因为函数也是对象,所以这很容易:
function makePerson(first, last) {
return {
first: first,
last: last,
fullName: function() {
return this.first + ' ' + this.last;
},
fullNameReversed: function() {
return this.last + ', ' + this.first;
}
};
}
s = makePerson('Simon', 'Willison');
console.log(s.fullName());
console.log(s.fullNameReversed());
VM1572:15 Simon Willison
VM1572:16 Willison, Simon
这里有一些我们前面没见过的东西:this 关键字
使用内部函数,this 指向当前对象
这实际上意味着你调用函数的方式来指定
如果你使用一个对象上的点符或括号符来调用,那么那个对象就是 this
如果调用没用点符,this 指向全局对象
注意 this 是一个频繁导致错误的东西,例如:
s = makePerson("Simon", "Willison");
var fullName = s.fullName;
fullName();
("undefined undefined");
当我们单独调用 fullName(),而不用 s.fullName(),this 被绑定在全局对象
因为没有全局变量叫 first 或者 last,所以我们对于二者都得到了 undefined
我们可以利用 this 关键字来提高我们的 makePerson 函数
function Person(first, last) {
this.first = first;
this.last = last;
this.fullName = function () {
return this.first + " " + this.last;
};
this.fullNameReversed = function () {
return this.last + " " + this.first;
};
}
var s = new Person("Simon", "Willison");
我们介绍了另一个关键字 new
new 和 this 强度关联
它创造一个新的空对象,然后调用指定的函数,并使用 this 设置给那个新建的对象
注意通过 this 指定的那个函数不返回一个值,但很少修改 this 对象
是 new 返回了 this 对象到调用的地方
被设计为通过 new 调用的函数称为构造函数
常见的做法是利用这些函数作为一个使用 new 调用它们的提醒
提高后的函数仍有和单独调用 fullName 相同的陷阱
我们的 person 对象变得更好了,但对于它们仍有一些丑陋边缘
每次我们创建一个 person 对象,我们都创建了其内的两个崭新的函数对象——如果这代码被分享不会变得更好吗?
function personFullName() {
return this.first + " " + this.last;
}
function personFullNameReversed() {
return this.last + ", " + this.first;
}
function Person(first, last) {
this.first = first;
this.last = last;
this.fullName = personFullName;
this.fullNameReversed = personFullNameReversed;
}
这变得更好了,我们只创建了函数一次,并且在构造器里给它们的引用赋值
我们能做的更好吗?答案是可以:
function Person(first, last) {
this.first = first;
this.last = last;
}
Person.prototype.fullName = function() {
return this.first + ' ' + this.last;
};
Person.prototype.fullNameReversed = function() {
return this.last + ', ' + this.first;
};
ƒ () {
return this.last + ', ' + this.first;
}
Person.prototype 是一个分享自 Person 所有实例的对象
它形成一个查找链的一部分(有一个特殊的名字,原型链):当任何时候你尝试去访问 Person 的一个属性时,JavaScript 会检查 Person.prototype 去看是否那个属性存在。
然后任何赋值给 Person.prototype 的东西对于构造器的所有实例经 this 对象变得可用
这是一个令人难以置信的强大工具
JavaScript 让你可以修改某个东西的原型在任何时候,在你的程序里,意味着你可以在运行时对已存在的对象添加额外的方法
s = new Person('Simon', 'Willison');
console.log(s.firstNameCaps());
VM587:2 Uncaught TypeError: s.firstNameCaps is not a function
Person.prototype.firstNameCaps = function() {
return this.first.toUpperCase();
};
console.log(s.firstNameCaps());
VM588:4 SIMON
有趣的是,你也可以向 JavaScript 内建对象添加东西
让我们给 String 添加一个方法以返回字符串的倒转字符串:
var s = 'Simon';
s.reversed();
VM620:2 Uncaught TypeError: s.reversed is not a function
String.prototype.reversed = function () {
var r = "";
for (var i = this.length - 1; i >= 0; i--) {
r += this[i];
}
return r;
};
s.reversed();
("nomiS");
我们的新方法甚至在字符串文字上有效!
"This can now be reversed.".reversed();
(".desrever eb won nac sihT");
像之前提到的,原型形成链的一部分
链根是 Object.prototype,它的方法包括 toString()——是这个方法被调用当你试图呈现一个对象为一个字符串时
这对于调试我们的 Person 对象很有用
var s = new Person("Simon", "Willison");
s.toString();
[object, object];
Person.prototype.toString = function () {
return "<Person: " + this.fullName() + ">";
};
s.toString();
("<Person: Simon Willison>");
记得 avg.apply 是如何获得一个 null 作为第一个参数的吗?
我们可以现在回看
apply()的第一个参数是应该被视为 this 的对象
例如:这里有一个粗糙的 new 实现
function trivialNew(constructor, ...args) {
var o = {};
constructor.apply(o, args);
return o;
}
这不是一个准确的 new 的复制品,因为没有建立原型链(很难说明这一点)
这不是你经常使用的东西,但知道这很有用
在这个片段,…args(包括省略号)被称为 rest 参数——正如其名暗示,这包含了参数余下的部分
var bill = trivialNew(Person, 'William', 'Orange');
undefined
bill
{first: "William", last: "Orange"}
所以这几乎等同于
var bill = new Person("William", "Orange");
apply()有一个姐妹叫做 call,再一次让你设置 this,但用一个不同于数组的拓展参数列表
function lastNameCaps() {
return this.last.toUpperCase();
}
var s = new Person("Simon", "Willison");
lastNameCaps.call(s);
("WILLISON");
这等同于:
s.lastNameCaps = lastNameCaps;
s.lastNameCaps();
("WILLISON");
内部函数:
JavaScript 函数声明允许在其他函数内部
我们之前见过一次,一个更早版本的 makePerson()函数
JavaScript 的嵌套函数的一个重要细节是他们可以访问它们父函数域的变量
function parentFunc() {
var a = 1;
function nestedFunc() {
var b = 4;
return a + b;
}
return nestedFunc();
}
这为写可维护代码提供了一个强大的处理工具
如果一个函数依赖一个或两个对你代码其他部分没有用的其他函数,你可以嵌套那些工具函数在函数内部,以被其他任何地方调用
这保持了全局域范围内的函数数量,总会是件好事
这也是一个强大的全局变量诱饵的计数器(原文:This is also a great counter to the lure of global variables.)
当写复杂代码时,经常尝试使用全局变量去在多个函数间分享——导致难以维护的代码
嵌套函数可以在它们的父内分享,所以你可以将那种机制用于对函数,当不污染你的全局命名空间讲得通时——“本地全局变量”
这个技术应当被小心使用,但它的确是一个有用能力
闭包
这带领我们走向 JavaScript 提供的最强大的抽象——但也是最潜在令人迷惑的
这是什么呢?
function makeAdder(a) {
return function(b) {
return a + b;
};
}
var x = makeAdder(5);
var y = makeAdder(20);
console.log(x(6));
console.log(y(7));
VM1955:8 11
VM1955:9 27
makeAdder 函数的名字应该放弃:它创造新的 adder 函数,每一个伴随着一个参数调用的函数,都将其添加到创建它的参数(原文:it creates new ‘adder’ functions, each of which, when called with one argument, adds it to the argument that it was created with.)
这里发生的事和内部函数非常相同:一个定义在另一个函数内部的函数访问了外部函数的变量
这里唯一不同的是外部函数有返回值,因此常识似乎指明它的局部变量不再存在
但它们仍存在——否则 adder 函数将无法工作
更重要的是,有 makeAdder()本地变量的两份不同的“拷贝”——一个在 a 中是 5,另一个 a 是 20
所以函数调用的结果是 11 和 27
这是真实正在发生的
无论何时 JavaScript 执行一个函数,“域”对象被创建来承载那个函数内部创建的本地变量
任何被传递进函数作为函数参数的变量将其初始化
这和承载全局变量和函数的全局对象类似,但一组不同的地方是:首先,一个崭新的域对象每次函数开始执行时被创建,其次,不像全局对象(类似通过 this 访问或浏览器中的 window),这些域对象在你的 JavaScript 代码中不能直接被访问到,比如没有机制被用来迭代当前域对象属性
所以当 makeAdder()被调用,一个域对象被创建,伴随一个属性 a,也就是被传给 makeAdder()的参数
makeAdder()然后返回一个新创建的函数
通常 JavaScript 的垃圾回收器会在这个点清除掉 makeAdder()创建的域对象,但返回的函数维护了一个引用到域对象
结果,域对象不会被垃圾回收器回收直到不再有 makeAdder()返回的函数对象的引用
域对象形成一个叫做域链的链,和 JavaScript 对象系统使用的原型链类似
闭包就是一个函数和其创造的域对象的集合体
闭包使你保持状态——如此,你会发现它们被用在对象的地方
see more closure@ closures
参考链接: