发表于: 2019-11-18 17:39:10
0 1131
js基础
代码结构
语句用分号分隔:
代码块 {...}
之后以及循环语句后不需要使用分号:
null vs 0
alert( null > 0 ); // (1) false
alert( null == 0 ); // (2) false
alert( null >= 0 ); // (3) true
进行值的比较时,null
会被转化为数字,因此它被转化为了 0
。这就是为什么(3)中 null >= 0
返回值是 true,(1)中 null > 0
返回值是 false。
但是, null
在相等性检测 ==
中不会进行任何的类型转换,它们有自己独立的比较规则,所以除了它们之间互等外,不会等于任何其他的值。这就解释了为什么(2)中 null == 0
会返回 false。
undefined
alert( undefined > 0 ); // false (1)
alert( undefined < 0 ); // false (2)
alert( undefined == 0 ); // false (3)
(1)
和(2)
都返回false
是因为undefined
在比较中被转换为了NaN
,而NaN
是一个特殊的数值型值,它与任何值进行比较都会返回false
。(3)
返回false
是因为这是一个相等性检测,而undefined
只与null
相等,不会与其他值相等。
- 规避错误
永远不要使用 >= > < <=
去比较一个可能为 null/undefined
的变量。对于取值可能是 null/undefined
的变量,请按需要分别检查它的取值情况。
注:如果函数参数未提供,则其(参数)值是 undefined
。
return
指令 return
可以在函数的任意位置。当执行到达时,函数停止,并将值返回给调用代码。
在没有值的情况下使用 return
可能会导致函数立即退出。
空值 return
或不带 return
返回 undefined
注:JavaScript 默认会在 return
之后加分号。不要在 return
与值之间添加一行(回车)。以避免它最后变成一个空值返回。
如果想要将返回的表达式跨行,应该在 return
的同一行开始写此表达式。或者至少添加一对括号将其围住。
“use strict”
"use strict"
指令将浏览器引擎转换为“现代”模式,改变一些内建特性的行为。我们会在之后的学习中了解这些细节。
严格模式通过将 "use strict"
放置在整个脚本或函数的顶部来启用。一些新语言特性诸如 “classes” 和 “modules” 也会自动开启严格模式。
所有的现代浏览器都支持严格模式。
我们建议始终使用 "use strict"
启动脚本。
交互
alert、prompt 和 confirm
alert(仅提示、确定)
confirm(提示、确定和取消)
prompt(提示、输入框、确定、取消) 接受两个参数:提示文本及输入框默认值
它们暂停脚本的执行,并且不允许用户与该页面的其余部分进行交互,直到窗口被解除。
上述所有方法共有两个限制:
- 模态窗口的确切位置由浏览器决定。通常在页面中心。
- 窗口的确切外观也取决于浏览器。我们不能修改它。
函数
命名
函数是行为。所以它们的名字通常是动词。它应该简短且尽可能准确地描述函数的作用。这样读代码的人就能得到关于该函数作用的指示。
一种普遍的做法是用动词前缀来开始一个函数,这个前缀模糊地描述了这个动作。团队内部必须就前缀的含义达成一致。
例如,以
"show"
开头的函数通常会显示某些内容。函数开始…
"get…"
—— 返回值,"calc…"
—— 计算"create…"
—— 创建,"check…"
—— 检查并返回 boolean 值,等。
注:两个独立的操作通常需要两个函数,即使它们通常被一起调用(在这种情况下,我们可以创建第三个函数来调用这两个函数)。
函数声明与箭头函数
为什么函数表达式结尾有一个 ;
,而函数声明没有:
答案很简单:
- 在代码块的结尾是不需要
;
,像if { ... }
,for { }
,function f { }
等语法结构后面都不用加。 - 函数表达式通常这样声明:
let sayHi = ...;
,作为一个变量。它不是代码块而是一段赋值语句。不管什么值,建议在语句结尾处建议使用分号;
。所以这里的分号与函数表达式本身没有任何关系,它只是终止了语句。
函数声明: 函数在主代码流中单独声明。
function sum(a, b){ } 使用函数声明可使它的调用先于声明。
函数表达式: 一个函数,在一个表达式中或另一个语法结构中创建。
let sum = function(a, b) { } 函数表达式在执行到达时创建,并只有从那时起才可用。
循环
可以通过 break
指令来终止。
如果我们不想在当前迭代中做任何事,并且想要转移至下一次迭代,那么可以使用 continue
指令。
break/continue
支持循环前的标签。标签是 break/continue
跳出嵌套循环以转到外部的唯一方法。
break <labelName>
(在函数内部执行)跳出<labelName>函数,执行函数之后的语句(可以跳出不只一层循环)
变量
我们可以使用 var
、let
或 const
声明变量来存储数据。
var
— 老旧的变量声明方式。一般情况下,我们不会再使用它。但是,我们会在 旧时的 "var" 章节介绍var
和let
的微妙差别,以防你需要它们。let
— 现代的变量声明方式。const
— 类似于let
,但是变量的值无法被修改。
变量应当以一种容易理解变量内部是什么的方式进行命名。
数据类型
有 7 种数据类型:
number
—— 可以是浮点数,也可以是整数,string
—— 字符串类型,boolean
—— 逻辑值:true/false
,null
—— 具有单个值null
的类型,表示”空“或“不存在”,undefined
—— 一个具有单个值undefined
的类型,表示「未分配」,object
和symbol
—— 对于复杂的数据结构和唯一标识符。
object
类型是一个特殊的类型。其他所有的数据类型都被称为“原生类型”,因为它们的值只包含一个单独的内容(字符串、数字或者其他)。
object
则用于储存数据集合和更复杂的实体。symbol
类型用于创建对象的唯一标识符。
typeof
运算符返回值的类型,但有两个例外:
typeof null == "object" // 语言的设计错误
typeof function(){} == "function" // 函数被特殊对待
对象
对象是具有一些特殊特性的关联数组。
他们存储键值对,其中:
- 属性的键必须是字符串或者Symbol(通常是字符串)。
- 值可以是任何类型。
我们可以用下面的方法获取属性:
- 点符号:
obj.property
。 - 方括号
obj["property"]
,方括号中可以使用变量obj[varWithKey]
。用来获取obj的各项属性,有较大的灵活性。
其他操作:
- 删除属性:
delete obj.prop
。 - 检查属性是否存在:
"key" in obj
。 - 遍历对象:
for(let key in obj)
循环。 - 法一:alert( user.noSuchProperty === undefined ); // true 意思是没有这个属性
- 法二:alert( "age" in user ); // true,user.age 存在
- 语法:"key" in object (in的左边必须是一个字符串)
对象根据引用来赋值或者复制。换句话说,变量存的不是对象的"值",而是值的 “引用”(内存地址)。 所以复制变量或者传递变量到方法中只是复制了对象的引用。 所有的引用操作(像增加,删除属性)都作用于同一个对象。
如果需要进行复制操作,这里有两种办法
法一、创建一份独立的拷贝。
需要创建一个新的对象,遍历现有对象的属性,在原始值的状态下复制给新的对象。
let clone = {}; // 新的空对象
// 复制所有的属性值
for (let key in user) {
clone[key] = user[key];
}
法二、用Object.assign 来实现
(let clone = Object.assign({}, user); 复制 user
对象所有的属性给了一个空对象,然后返回拷贝后的对象。)
语法:Object.assign(dest,[ src1, src2, src3...])- 参数
dest
和 src1, ..., srcN
(可以有很多个)是对象。
dest
和 src1, ..., srcN
(可以有很多个)是对象。- 这个方法复制了
src1, ..., srcN
的所有对象到 dest
。换句话说,从第二个参数开始,所有对象的属性都复制给了第一个参数对象,然后返回 dest
。
src1, ..., srcN
的所有对象到 dest
。换句话说,从第二个参数开始,所有对象的属性都复制给了第一个参数对象,然后返回 dest
。 如果接收的对象(user
)已经有了同样属性名的属性,前面的会被覆盖:
但如果对象属性指向对象,即对象内包含了一个对象,Object.assign后,他们共享了这个对象,为了解决这个问题,我们在复制的时候应该检查 user[key]
的每一个值,如果是一个对象,我们再复制一遍这个对象,这叫做深拷贝。
深拷贝的话我们可以使用 Object.assign
或者 _.cloneDeep(obj)。
JavaScript 中还有很多其他类型的对象:
Array
存储有序数据集合。Date
存储时间日期。Error
存储错误信息- …等等。
Symbol 类型
根据规范,对象的属性键只能是 String 类型或者 Symbol 类型。
“Symbol” 值表示唯一的标识符。
可以使用 Symbol()
来创建这种类型的值:
// id 是 symbol 的一个实例化对象
let id = Symbol();
我们可以给 Symbol 一个描述(也称为 Symbol 名),这对于调试非常有用:
// id 是描述为 "id" 的 Symbol
let id = Symbol("id");
Symbol 保证是唯一的。即使我们创建了许多具有相同描述的 Symbol,它们的值也是不同。描述只是一个不影响任何东西的标签。
例如,这里有两个描述相同的 Symbol —— 它们不相等:
let id1 = Symbol("id");
let id2 = Symbol("id");
alert(id1 == id2); // false
JavaScript 中的大多数值都支持 string 的隐式转换。例如,我们可以 alert
任何值,这会起作用。Symbol 是特别的,它无法自动转换。
例如,这个 alert
将会显示错误:
let id = Symbol("id");
alert(id); // 类型错误:无法将 Symbol 值转换为 String。
如果我们真的想显示一个 Symbol,我们需要在它上面调用 .toString()
,如下所示:
let id = Symbol("id");
alert(id.toString()); // Symbol(id),现在它起作用了
这是一种防止混乱的“语言保护”,因为 String 和 Symbol 有本质上的不同,而且不应该偶尔将它们相互转化。
“隐藏”属性
Symbol 允许我们创建对象的“隐藏”属性,代码的任何其他部分都不能偶尔访问或重写这些属性。
例如,如果我们想存储 object user
的“标识符”,我们可以使用 Symbol 作为它的键:
let user = { name: "John" };
let id = Symbol("id");
user[id] = "ID Value";
alert( user[id] ); // 我们可以使用 Symbol 作为键来访问数据。
在 string "id"
上使用 Symbol("id")
有什么好处?
我们用更深入一点的示例来说明这一点。
假设另一个脚本希望 user
中有它自己的 “id” 属性可以操作。这可能是另一个 JavaScript 库,所以这些脚本完全不知道对方是谁。
然后该脚本可以创建自己的 Symbol("id")
,如下所示:
let id = Symbol("id");
user[id] = "Their id value";
不会冲突,因为 Symbol 总是不同的,即使它们有相同的名称。
现在请注意,如果我们使用 String "id"
而不是用 symbol,那么就会出现冲突:
let user = { name: "John" };
//我们的脚本使用 "id" 属性。
user.id = "ID Value";
// ...如果之后另一个脚本为其目的使用 "id"...
user.id = "Their id value"
// 砰!无意中重写了 id!他不是故意伤害同事的,而是这样做了!
字面量中的 Symbol
如果我们要在 object 字面量中使用 Symbol,则需要方括号。
let id = Symbol("id");
let user = {
name: "John",
[id]: 123 // 不仅仅是 "id:123"
};
这是因为我们需要变量 id
的值作为键,而不是 String “id”。
Symbol 在 for…in 中被跳过
Symbolic 属性不参与 for..in
循环。
let id = Symbol("id");
let user = {
name: "John",
age: 30,
[id]: 123
};
for (let key in user) alert(key); // name, age (没有 symbols)
// 被 Symbol 任务直接访问
alert( "Direct: " + user[id] );
这是一般“隐藏”概念的一部分。如果另一个脚本或库在我们的对象上循环,它不会访问一个 Symbol 类型的属性。
相反,Object.assign 同时复制字符串和符号属性:当我们克隆一个 object 或合并 object 时,通常所有属性会被复制(包括像 id
这样的 Symbol)。
全局 symbol
有时,我们希望在应用程序的不同部分访问相同的 Symbol "id"
属性。
为此,存在一个全局 symbol 注册表。我们可以在其中创建 Symbol 并在稍后访问它们,它可以确保每次访问相同名称都会返回相同的 Symbol。
为了在注册表中创建或读取 Symbol,请使用 Symbol.for(key)
。
该调用会检查全局注册表,如果有一个描述为 key
的 Symbol,则返回该 Symbol,否则将创建一个新 Symbol(Symbol(key)
),并通过给定的 key
将其存储在注册表中。
// 从全局注册表中读取
let id = Symbol.for("id"); // 如果该 Symbol 不存在,则创建它
// 再次读取
let idAgain = Symbol.for("id");
// 相同的 Symbol
alert( id === idAgain ); // true
注册表内的 Symbol 称为全局 Symbol。如果我们想要一个应用程序范围内的 Symbol,可以在代码中随处访问
Symbol.keyFor
对于全局 symbol,Symbol.for(key)
不仅按名称返回一个 symbol,而且还有一个反向调用:Symbol.keyFor(sym)
,反过来:通过全局 symbol 返回一个名称。
let sym = Symbol.for("name");
let sym2 = Symbol.for("id");
// 从 symbol 中获取 name
alert( Symbol.keyFor(sym) ); // name
alert( Symbol.keyFor(sym2) ); // id
Symbol.keyFor
在内部使用全局 symbol 注册表来查找 symbol 的键。所以它不适用于非全局 symbol。如果 symbol 不是全局的,它将无法找到它并返回 undefined
系统 Symbol
JavaScript 内部存在很多“系统” Symbol,我们可以使用它们来微调对象的各个方面。
它们列在熟悉的 Symbol 表的规范中:
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.iterator
Symbol.toPrimitive
- …等等。
总结
Symbol
是唯一标识符的基本类型
Symbol 使用 Symbol()
创建的,调用带有一个可选的描述。
Symbol 总是不同的值,即使它们有相同的名称。如果我们希望同名 Symbol 相等,那么我们应该使用全局注册表:Symbol.for(key)
返回(如果需要的话创建)一个以 key
作为名称的全局 Symbol。Symbol.for
的多次调用完全返回相同的 Symbol。
Symbol 有两个主要的使用场景:
“隐藏” 对象属性。如果需要将属性添加到 “属于” 另一个脚本或库的对象中,则可以创建 Symbol 并将其用作属性键。Symbol 属性不出现在
for..in
中,因此不会无心列出。另外,它不会被直接访问,因为另一个脚本没有我们的符号,所以它不会不小心干预它的操作。因此我们可以使用 Symbol 属性“秘密地”将一些东西隐藏到我们需要的对象中,但其他人不会以对象属性的形式看到它。
JavaScript 使用了许多系统 Symbol,这些 Symbol 可以作为
Symbol.*
访问。我们可以使用它们来改变一些内置行为。例如,在本教程的后面部分,我们将使用Symbol.iterator
来迭代,Symbol.toPrimitive
来设置对象原始值的转换等等。
从技术上说,Symbol 不是 100% 隐藏的。有一个内置方法 Object.getOwnPropertySymbols(obj) 允许我们获取所有的 Symbol。还有一个名为 Reflect.ownKeys(obj) 返回所有键,包括 Symbol。所以它们不是真正的隐藏。但是大多数库、内置方法和语法结构都遵循一个共同的协议。而明确调用上述方法的人可能很清楚他在做什么。
对象原始值转换(https://zh.javascript.info/object-toprimitive)
ToPrimitive
对象到原始值的转换,是由许多内置函数和操作符自动调用的,这些函数使用一个原始值作为返回值的。
取决于上下文,转换具有所谓的“暗示”。它有三种类型(暗示):
"string"
(对于alert
和其他字符串转换)"number"
(对于maths
)"default"
(少数操作)
规范明确描述了哪个操作符使用哪个暗示。极少数操作者“不知道期望什么”并使用 "default"
暗示。通常对于内置对象,"default"
暗示的处理方式与 "number"
相同,因此在实践中最后两个通常合并在一起。
转换算法是:
- 调用
obj[Symbol.toPrimitive](hint)
如果这个方法存在的话, - 否则如果暗示是
"string"
- 尝试
obj.toString()
和obj.valueOf()
,无论哪个存在。
- 尝试
- 否则,如果暗示
"number"
或者"default"
- 尝试
obj.valueOf()
和obj.toString()
,无论哪个存在。
- 尝试
在实践中,为了记录或调试目的,仅实现 obj.toString()
作为“全捕获"方法通常就够了,这样所有转换都能返回一种“人类可读”的对象表达形式。
对象方法与 "this"
对象通常被用来表示真实世界中的实体,比如用户、订单等等:
另外,在现实世界中,用户可以操作:从购物车中挑选某物、登录、注销等。
在 JavaScript 中,操作通过属性中的函数来表示。
作为对象属性的函数称之为方法。
我们可以使用函数表达式创建函数,并将其指定给对象的 user.sayHi
属性。
let user = {
name: "John",
age: 30
};
user.sayHi = function() {
alert("Hello!");
};
user.sayHi(); // Hello!
那么,现在 user
对象有了一个 sayHi
方法。
也可以使用预先定义的函数作为方法
let user = {
// ...
};
// 首先声明
function sayHi() {
alert("Hello!");
};
// 然后将其作为一个方法
user.sayHi = sayHi;
user.sayHi(); // Hello!
方法中的 “this”
在很多时候,对象方法需要访问对象中的存储的信息来完成其工作。
举个例子,user.sayHi()
中的代码可能需要用到 user
的 name 属性。
为了访问该对象,方法中可以使用 this
关键字。
this
的值就是在点之前的这个对象,即调用该方法的对象。
let user = {
name: "John",
age: 30,
sayHi() {
alert(this.name);
}
};
user.sayHi(); // John
在这里 user.sayHi()
执行过程中,this
的值是 user
。
技术上讲,也可以在不使用 this
的情况下,通过外部变量名来引用它:
“this” 不受限制
在 JavaScript 中,“this” 关键字与大多数其他编程语言中的不同。首先,它可以用于任何函数。
这样的代码没有语法错误:
function sayHi() {
alert( this.name );
}
this
是在运行时求值的。它可以是任何值。
例如,从不同的对象中调用同一个函数可能会有不同的 “this” 值:
let user = { name: "John" };
let admin = { name: "Admin" };
function sayHi() {
alert( this.name );
}
// 在两个对象中使用的是相同的函数
user.f = sayHi;
admin.f = sayHi;
// 它们调用时有不同的 this 值。
// 函数内部的 "this" 是点之前的这个对象。
user.f(); // John (this == user)
admin.f(); // Admin (this == admin)
admin['f'](); // Admin(使用点或方括号语法来访问这个方法,都没有关系。)
内部:引用类型(https://zh.javascript.info/object-methods#nei-bu-yin-yong-lei-xing)
总结
- 存储在对象中函数称之为『方法』。
- 对象执行方法进行『操作』,比如
object.doSomething()
。 - 方法可以将该对象引用为
this
。
this
的值是在运行时求值的。
- 函数声明使用的
this
只有等到调用时才会有值。 - 函数可以在对象之间进行共用。
- 当函数使用『方法』语法
object.method()
调用时,调用过程中的this
总是指向object
。
请注意箭头函数有些特别:它们没有 this
。在箭头函数内部访问的都是来自外部的 this
值。
任务(https://zh.javascript.info/object-methods#tasks)
let user = {
name: "John",
go: function() { alert(this.name) }
}
(user.go)()
// error!
大多数浏览器中的错误信息并不能说明出现了什么问题。
出现此错误是因为在 user = {...}
之后遗漏了一个分号。
JavaScript 不会在括号 (user.go)()
前自动插入分号,所以解析的代码如下:
let user = { go:... }(user.go)()
那么,我们可以看到这样一个连接的表达式,在语法构成上,把对象 { go: ... }
作为一个方法调用,并且传递的参数为 (user.go)
。并且让 let user
在同一行赋值,因此 user
没被定义(之前)就会出现错误
如果我们插入该分号,一切都会正常:
let user = {
name: "John",
go: function() { alert(this.name) }
};
(user.go)() // John
要注意的是 (user.go)
内的括号没有什么意义。通常用它们来设置操作的顺序,但在这里点 .
总是会先执行,所以并没有什么影响。分号是唯一重要的。
创建一个有三个方法的 calculator
对象:
read()
提示输入两个值,将其保存为对象属性。sum()
返回保存的值的和。mul()
将保存的值相乘并返回其结果。
let calculator = {
sum() {
return this.a + this.b;
},
mul() {
return this.a * this.b;
},
read() {
this.a = +prompt('a?', 0);
this.b = +prompt('b?', 0);
}
};
calculator.read();
alert( calculator.sum() );
alert( calculator.mul() );
评论