JavaScript笔记(三)| 青训营笔记

92 阅读14分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 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特性

变量

可以使用以下方式声明变量:

  • let
  • const(不变的,不能被改变)
  • var(旧式的,稍后会看到)

有 8 种数据类型:

  • number — 可以是浮点数,也可以是整数,
  • bigint — 用于任意长度的整数,
  • string — 字符串类型,
  • boolean — 逻辑值:true/false
  • null — 具有单个值 null 的类型,表示“空”或“不存在”,
  • undefined — 具有单个值 undefined 的类型,表示“未分配(未定义)”,
  • objectsymbol — 对于复杂的数据结构和唯一标识符,我们目前还没学习这个类型。

typeof 运算符返回值的类型,但有两个例外:

typeof null == "object" // JavaScript 编程语言的设计错误
typeof function(){} == "function" // 函数被特殊对待

交互

prompt(question[, default])

提出一个问题 question,并返回访问者输入的内容,如果他按下「取消」则返回 null

confirm(question)

提出一个问题 question,并建议用户在“确定”和“取消”之间进行选择。选择结果以 true/false 形式返回。

alert(message)

输出一个消息 message

比较运算符

对不同类型的值进行相等检查时,运算符 == 会将不同类型的值转换为数字(除了 nullundefined,它们彼此相等而没有其他情况),所以下面的例子是相等的:

alert( 0 == false ); // true
alert( 0 == '' ); // true

其他比较也将转换为数字。

严格相等运算符 === 不会进行转换:不同的类型总是指不同的值。

nullundefined 是特殊的:它们只在 == 下相等,且不相等于其他任何值。

大于/小于比较,在比较字符串时,会按照字符顺序逐个字符地进行比较。其他类型则被转换为数字。

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; // 复制引用

image-20230205095443671

仅当两个对象为同一对象时,两者才相等。

例如,这里 ab 两个变量都引用同一个对象,所以它们相等:

let a = {};
let b = a; // 复制引用
​
alert( a == b ); // true,都引用同一对象
alert( a === b ); // true

而这里两个独立的对象则并不相等,即使它们看起来很像(都为空):

let a = {};
let b = {}; // 两个独立的对象
​
alert( a == b ); // false

对象的克隆与拷贝

如果我们想要复制一个对象,那该怎么做呢?

  1. 可以创建一个新对象,通过遍历已有对象的属性,并在原始类型值的层面复制它们,以实现对已有对象结构的复制。
  2. 也可以使用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 是个对象,它会以引用形式被拷贝。因此 cloneuser 会共用一个 sizes

应该使用一个拷贝循环来检查 user[key] 的每个值,如果它是一个对象,那么也复制它的结构。这就是所谓的“深拷贝”。

使用 const 声明的对象也是可以被修改的

const user = {
  name: "John"
};user.name = "Pete"; // (*)
​
alert(user.name); // Pete

user 的值是一个常量,它必须始终引用同一个对象,但该对象的属性可以被自由修改。

换句话说,只有当我们尝试将 user=... 作为一个整体进行赋值时,const user 才会报错。

垃圾回收

可达性

JavaScript 中主要的内存管理概念是 可达性。固有的可达值的基本集合,这些值明显不能被释放。

  1. 这里列出固有的可达值的基本集合,这些值明显不能被释放。

比方说:

  • 当前执行的函数,它的局部变量和参数。
  • 当前嵌套调用链上的其他函数、它们的局部变量和参数。
  • 全局变量。
  • (还有一些内部的)

这些值被称作 根(roots)

  1. 如果一个值可以通过引用链从根访问任何其他值,则认为该值是可达的。

比方说,如果全局变量中有一个对象,并且该对象有一个属性引用了另一个对象,则 对象被认为是可达的。而且它引用的内容也是可达的。

在 JavaScript 引擎中有一个被称作 垃圾回收器 的东西在后台执行。它监控着所有对象的状态,并删除掉那些已经不可达的。

this

构造器和操作符"new"

构造函数

构造函数在技术上是常规函数。不过有两个约定:

  1. 它们的命名以大写字母开头。
  2. 它们只能由 "new" 操作符来执行
function User(name) {
  this.name = name;
  this.isAdmin = false;
}
​
let user = new User("Jack");
​
alert(user.name); // Jack
alert(user.isAdmin); // false

当一个函数被使用 new 操作符执行时,它按照以下步骤:

  1. 一个新的空对象被创建并分配给 this
  2. 函数体执行。通常它会修改 this,为其添加新的属性。
  3. 返回 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 相同,
  • 否则(当 valueundefined/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) { ... };

默认情况下,普通对象具有 toStringvalueOf 方法:

  • toString 方法返回一个字符串 "[object Object]"
  • valueOf 方法返回对象自身。

转换算法是:

  1. 调用 obj[Symbol.toPrimitive](hint) 如果这个方法存在,

  2. 否则,如果 hint 是

    "string"
    
    • 尝试调用 obj.toString()obj.valueOf(),无论哪个存在。
  3. 否则,如果 hint 是

    "number"
    

    或者

    "default"
    
    • 尝试调用 obj.valueOf()obj.toString(),无论哪个存在。

所有这些方法都必须返回一个原始值才能工作(如果已定义)。