这是我参与「第五届青训营 」伴学笔记创作活动的第 7 天
JavaScript函数
函数
函数声明
例子:
function showMessage() {
alert( 'Hello everyone!' );
}
我们有一个变量 from,并将它传递给函数。请注意:函数会修改 from,但在函数外部看不到更改,因为函数修改的是复制的变量值副本:
function showMessage(from, text) {
from = '*' + from + '*'; // 让 "from" 看起来更优雅
alert( from + ': ' + text );
}
let from = "Ann";
showMessage(from, "Hello"); // *Ann*: Hello
// "from" 值相同,函数修改了一个局部的副本。
alert( from ); // Ann
默认值
如果一个函数被调用,但是参数(argument)没有被提供,那么相应的值就会变成undefined
我们可以使用 = 为函数声明中的参数指定所谓的“默认”(如果对应参数的值未被传递则使用)值:
function showMessage(from, text = "no text given") {
函数表达式
另一种创建函数的语法称为 函数表达式
let sayHi = function() {
alert( "Hello" );
};
变量 sayHi 得到了一个值,新函数 function() { alert("Hello"); }
由于函数创建发生在赋值表达式的上下文中(在 = 的右侧),因此这是一个 函数表达式。
这里是立即把它赋值给变量:创建一个变量并将其放进sayhi中,同样的,可以创建一个函数并立即调用
重申一次:无论函数是如何创建的,函数都是一个值。上面的两个示例都在 sayHi 变量中存储了一个函数。
没有括号就不会调用
这样的写法最后面要加分号(很简单理解
回调函数
function ask(question, yes, no) {
if (confirm(question)) yes()
else no();
}
function showOk() {
alert( "You agreed." );
}
function showCancel() {
alert( "You canceled the execution." );
}
// 用法:函数 showOk 和 showCancel 被作为参数传入到 ask
ask("Do you agree?", showOk, showCancel);
showOk和showCancel就是yes和no的回调函数
还有这种匿名函数的写法,更简洁:
function ask(question, yes, no) {
if (confirm(question)) yes()
else no();
}
ask(
"Do you agree?",
function() { alert("You agreed."); },
function() { alert("You canceled the execution."); }
);
一个函数是表示一个“行为”的值
字符串或数字等常规值代表 数据。
函数可以被视为一个 行为(action) 。
我们可以在变量之间传递它们,并在需要时运行。
区别
函数表达式是执行到的时候才会创建
函数声明是脚本运行时被创建的,在任何位置都可以看到
函数声明只在它所在的代码块中可见
箭头函数
let func = (arg1, arg2, ..., argN) => expression;
这里创建了一个函数 func,它接受参数 arg1..argN,然后使用参数对右侧的 expression 求值并返回其结果。
换句话说,它是下面这段代码的更短的版本:
let func = function(arg1, arg2, ..., argN) {
return expression;
};
例子:
let sum = (a,b)=>a+b;
- 如果我们只有一个参数,还可以省略掉参数外的圆括号,使代码更短。
let double = n => n * 2;
- 如果没有参数,括号则是空的(但括号必须保留):
let sayHi = () => alert("Hello!");
-
也可以像函数表达式一样用
let age = prompt("What is your age?", 18); let welcome = (age < 18) ? () => alert('Hello!') : () => alert("Greetings!"); welcome(); -
如果使用了花括号就需要一个显式的return
let sum = (a, b) => { // 花括号表示开始一个多行函数 let result = a + b; return result; // 如果我们使用了花括号,那么我们需要一个显式的 “return” };
JavaScript特性
变量
可以使用以下方式声明变量:
letconst(不变的,不能被改变)var(旧式的,稍后会看到)
有 8 种数据类型:
number— 可以是浮点数,也可以是整数,bigint— 用于任意长度的整数,string— 字符串类型,boolean— 逻辑值:true/false,null— 具有单个值null的类型,表示“空”或“不存在”,undefined— 具有单个值undefined的类型,表示“未分配(未定义)”,object和symbol— 对于复杂的数据结构和唯一标识符,我们目前还没学习这个类型。
typeof 运算符返回值的类型,但有两个例外:
typeof null == "object" // JavaScript 编程语言的设计错误
typeof function(){} == "function" // 函数被特殊对待
交互
提出一个问题 question,并返回访问者输入的内容,如果他按下「取消」则返回 null。
提出一个问题 question,并建议用户在“确定”和“取消”之间进行选择。选择结果以 true/false 形式返回。
输出一个消息 message。
比较运算符
对不同类型的值进行相等检查时,运算符 == 会将不同类型的值转换为数字(除了 null 和 undefined,它们彼此相等而没有其他情况),所以下面的例子是相等的:
alert( 0 == false ); // true
alert( 0 == '' ); // true
其他比较也将转换为数字。
严格相等运算符 === 不会进行转换:不同的类型总是指不同的值。
值 null 和 undefined 是特殊的:它们只在 == 下相等,且不相等于其他任何值。
大于/小于比较,在比较字符串时,会按照字符顺序逐个字符地进行比较。其他类型则被转换为数字。
JavaScript对象
对象
对象用来存储键值对和更复杂的实体
-
创建对象
let user = new Object(); // “构造函数” 的语法 let user = {}; // “字面量” 的语法 -
.访问属性值
-
delete 移除属性
-
多词属性(必须加引号
-
属性列表最后跟逗号
-
方括号
-
属性名简写
-
属性判断
alert( user.noSuchProperty === undefined ); // true 意思是没有这个属性 alert( "age" in user ); // true,user.age 存在 alert( "blabla" in user ); // false,user.blabla 不存在for in 循环
for (key in object) { // 对此对象属性中的每个键执行的代码 }所有的 “for” 结构体都允许我们在循环中定义变量,像这里的
let key。同样,我们可以用其他属性名来替代
key。例如"for(let prop in obj)"也很常用。 -
排序
整数属性会被进行排序,其他属性则按照创建的顺序显示
属性名是整数就会按照这个排名,如果不想要,那就弄个+号,如下:
let codes = { "+49": "Germany", "+41": "Switzerland", "+44": "Great Britain", // .., "+1": "USA" }; for (let code in codes) { alert( +code ); // 49, 41, 44, 1 }
对象的引用和复制
赋值了对象的变量存储的不是对象本身,而是该对象“在内存中的地址” —— 换句话说就是对该对象的“引用”。
当一个对象变量被复制 —— 引用被复制,而该对象自身并没有被复制。
let user = { name: "John" };
let admin = user; // 复制引用
仅当两个对象为同一对象时,两者才相等。
例如,这里 a 和 b 两个变量都引用同一个对象,所以它们相等:
let a = {};
let b = a; // 复制引用
alert( a == b ); // true,都引用同一对象
alert( a === b ); // true
而这里两个独立的对象则并不相等,即使它们看起来很像(都为空):
let a = {};
let b = {}; // 两个独立的对象
alert( a == b ); // false
对象的克隆与拷贝
如果我们想要复制一个对象,那该怎么做呢?
- 可以创建一个新对象,通过遍历已有对象的属性,并在原始类型值的层面复制它们,以实现对已有对象结构的复制。
- 也可以使用object.assign方法
Object.assign(dest, [src1, src2, src3...])
- 第一个参数
dest是指目标对象。 - 更后面的参数
src1, ..., srcN(可按需传递多个参数)是源对象。 - 该方法将所有源对象的属性拷贝到目标对象
dest中。换句话说,从第二个开始的所有参数的属性都被拷贝到第一个参数的对象中。 - 调用结果返回
dest。
可以用来合并多个对象:
// 将 permissions1 和 permissions2 中的所有属性都拷贝到 user 中
Object.assign(user, permissions1, permissions2);、
可以代替for..in循环进行简单克隆:
let clone = Object.assign({}, user);
它将 user 中的所有属性拷贝到了一个空对象中,并返回这个新的对象。
对象的深层克隆
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
现在这样拷贝 clone.sizes = user.sizes 已经不足够了,因为 user.sizes 是个对象,它会以引用形式被拷贝。因此 clone 和 user 会共用一个 sizes
应该使用一个拷贝循环来检查 user[key] 的每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的“深拷贝”。
使用 const 声明的对象也是可以被修改的
const user = {
name: "John"
};
user.name = "Pete"; // (*)
alert(user.name); // Pete
user 的值是一个常量,它必须始终引用同一个对象,但该对象的属性可以被自由修改。
换句话说,只有当我们尝试将 user=... 作为一个整体进行赋值时,const user 才会报错。
垃圾回收
可达性
JavaScript 中主要的内存管理概念是 可达性。固有的可达值的基本集合,这些值明显不能被释放。
- 这里列出固有的可达值的基本集合,这些值明显不能被释放。
比方说:
- 当前执行的函数,它的局部变量和参数。
- 当前嵌套调用链上的其他函数、它们的局部变量和参数。
- 全局变量。
- (还有一些内部的)
这些值被称作 根(roots)
- 如果一个值可以通过引用链从根访问任何其他值,则认为该值是可达的。
比方说,如果全局变量中有一个对象,并且该对象有一个属性引用了另一个对象,则 该 对象被认为是可达的。而且它引用的内容也是可达的。
在 JavaScript 引擎中有一个被称作 垃圾回收器 的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。
this
构造器和操作符"new"
构造函数
构造函数在技术上是常规函数。不过有两个约定:
- 它们的命名以大写字母开头。
- 它们只能由
"new"操作符来执行
function User(name) {
this.name = name;
this.isAdmin = false;
}
let user = new User("Jack");
alert(user.name); // Jack
alert(user.isAdmin); // false
当一个函数被使用 new 操作符执行时,它按照以下步骤:
- 一个新的空对象被创建并分配给
this。 - 函数体执行。通常它会修改
this,为其添加新的属性。 - 返回
this的值。
换句话说,new User(...) 做的就是类似的事情:
function User(name) {
// this = {};(隐式创建)
// 添加属性到 this
this.name = name;
this.isAdmin = false;
// return this;(隐式返回)
}
构造器模式测试
- 构造函数,或简称构造器,就是常规函数,但大家对于构造器有个共同的约定,就是其命名首字母要大写。
- 构造函数只能使用
new来调用。这样的调用意味着在开始时创建了空的this,并在最后返回填充了值的this。
我们可以使用构造函数来创建多个类似的对象。
JavaScript 为许多内建的对象提供了构造函数:比如日期 Date、集合 Set 以及其他我们计划学习的内容。
可选链"?."
可选链 ?. 是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误。
我们大多数用户的地址都存储在 user.address 中,街道地址存储在 user.address.street 中,但有些用户没有提供这些信息。
在这种情况下,当我们尝试获取 user.address.street,而该用户恰好没提供地址信息,我们则会收到一个错误:
let user = {}; // 一个没有 "address" 属性的 user 对象
alert(user.address.street); // Error!
我们希望避免出现这种错误,而是接受 = null 作为结果。
最先想到的方案是在访问该值的属性之前,使用 if 或条件运算符 ? 对该值进行检查,像这样:
let user = {};
alert(user.address ? user.address.street : undefined);
如果可选链 ?. 前面的值为 undefined 或者 null,它会停止运算并返回 undefined。
换句话说,例如 value?.prop:
- 如果
value存在,则结果与value.prop相同, - 否则(当
value为undefined/null时)则返回undefined。
下面这是一种使用 ?. 安全地访问 user.address.street 的方式:
let user = {}; // user 没有 address 属性
alert( user?.address?.street ); // undefined(不报错)
代码简洁明了,也不用重复写好几遍属性名。
这里是一个结合 document.querySelector 使用的示例:
let html = document.querySelector('.elem')?.innerHTML; // 如果没有符合的元素,则为 undefined
即使 对象 user 不存在,使用 user?.address 来读取地址也没问题:
let user = null;
alert( user?.address ); // undefined
alert( user?.address.street ); // undefined
不要过度使用可选链
我们应该只将
?.使用在一些东西可以不存在的地方。例如,如果根据我们的代码逻辑,
user对象必须存在,但address是可选的,那么我们应该这样写user.address?.street,而不是这样user?.address?.street。那么,如果
user恰巧为 undefined,我们会看到一个编程错误并修复它。否则,如果我们滥用?.,会导致代码中的错误在不应该被消除的地方消除了,这会导致调试更加困难。
?. 前的变量必须已声明
如果未声明变量 user,那么 user?.anything 会触发一个错误:
// ReferenceError: user is not defined
user?.address;
?. 前的变量必须已声明(例如 let/const/var user 或作为一个函数参数)。可选链仅适用于已声明的变量。
可选链 ?. 不是一个运算符,而是一个特殊的语法结构。它还可以与函数和方括号一起使用。
例如,将 ?.() 用于调用一个可能不存在的函数。
在下面这段代码中,有些用户具有 admin 方法,而有些没有:
let userAdmin = {
admin() {
alert("I am admin");
}
};
let userGuest = {};
userAdmin.admin?.(); // I am admin
userGuest.admin?.(); // 啥都没发生(没有这样的方法)
-
总结
可选链
?.不是一个运算符,而是一个特殊的语法结构。它还可以与函数和方括号一起使用。例如,将
?.()用于调用一个可能不存在的函数。在下面这段代码中,有些用户具有
admin方法,而有些没有:let userAdmin = { admin() { alert("I am admin"); } }; let userGuest = {}; userAdmin.admin?.(); // I am admin userGuest.admin?.(); // 啥都没发生(没有这样的方法)
symbol类型
根据规范,只有两种原始类型可以用作对象属性键:
-
字符串类型
-
symbol 类型
“symbol” 值表示唯一的标识符。
可以使用
Symbol()来创建这种类型的值:let id = Symbol();创建时,我们可以给 symbol 一个描述(也称为 symbol 名),这在代码调试时非常有用:
// id 是描述为 "id" 的 symbol let id = Symbol("id");可用来隐藏属性
let user = { // 属于另一个代码 name: "John" }; let id = Symbol("id"); user[id] = 1; alert( user[id] ); // 我们可以使用 symbol 作为键来访问数据由于
user对象属于另一个代码库,所以向它们添加字段是不安全的,因为我们可能会影响代码库中的其他预定义行为。但 symbol 属性不会被意外访问到。第三方代码不会知道新定义的 symbol,因此将 symbol 添加到user对象是安全的。
对象原始值转换
对象会被自动转换为原始值,然后对这些原始值进行运算,并得到运算结果(也是一个原始值)。
这是一个重要的限制:因为 obj1 + obj2(或者其他数学运算)的结果不能是另一个对象!
对象如何转换为原始值:
类型转换在各种情况下有三种变体。它们被称为 “hint”,在 规范 所述:
-
"string"对象到字符串的转换,当我们对期望一个字符串的对象执行操作时,如 “alert”:
// 输出 alert(obj); // 将对象作为属性键 anotherObj[obj] = 123; -
"number"对象到数字的转换,例如当我们进行数学运算时:
// 显式转换 let num = Number(obj); // 数学运算(除了二元加法) let n = +obj; // 一元加法 let delta = date1 - date2; // 小于/大于的比较 let greater = user1 > user2;大多数内建的数学函数也包括这种转换。 -
"default"在少数情况下发生,当运算符“不确定”期望值的类型时。例如,二元加法
+可用于字符串(连接),也可以用于数字(相加)。因此,当二元加法得到对象类型的参数时,它将依据"default"hint 来对其进行转换。此外,如果对象被用于与字符串、数字或 symbol 进行==比较,这时到底应该进行哪种转换也不是很明确,因此使用"default"hint。// 二元加法使用默认 hint let total = obj1 + obj2; // obj == number 使用默认 hint if (user == 1) { ... };
默认情况下,普通对象具有 toString 和 valueOf 方法:
toString方法返回一个字符串"[object Object]"。valueOf方法返回对象自身。
转换算法是:
-
调用
obj[Symbol.toPrimitive](hint)如果这个方法存在, -
否则,如果 hint 是
"string"- 尝试调用
obj.toString()或obj.valueOf(),无论哪个存在。
- 尝试调用
-
否则,如果 hint 是
"number"或者
"default"- 尝试调用
obj.valueOf()或obj.toString(),无论哪个存在。
- 尝试调用
所有这些方法都必须返回一个原始值才能工作(如果已定义)。