发表于: 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. (1) 和 (2) 都返回 false 是因为 undefined 在比较中被转换为了 NaN,而 NaN 是一个特殊的数值型值,它与任何值进行比较都会返回 false
  2. (3) 返回 false 是因为这是一个相等性检测,而 undefined 只与 null 相等,不会与其他值相等。

  1. 规避错误

永远不要使用 >= > < <= 去比较一个可能为 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(提示、输入框、确定、取消) 接受两个参数:提示文本及输入框默认值

它们暂停脚本的执行,并且不允许用户与该页面的其余部分进行交互,直到窗口被解除。

上述所有方法共有两个限制:

  1. 模态窗口的确切位置由浏览器决定。通常在页面中心。
  2. 窗口的确切外观也取决于浏览器。我们不能修改它。

函数

命名

函数是行为。所以它们的名字通常是动词。它应该简短且尽可能准确地描述函数的作用。这样读代码的人就能得到关于该函数作用的指示。

一种普遍的做法是用动词前缀来开始一个函数,这个前缀模糊地描述了这个动作。团队内部必须就前缀的含义达成一致。

  1. 例如,以 "show" 开头的函数通常会显示某些内容。

  2. 函数开始…

  1. "get…" —— 返回值,
  2. "calc…" —— 计算
  3. "create…" —— 创建,
  4. "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>函数,执行函数之后的语句(可以跳出不只一层循环)

变量

我们可以使用 varlet 或 const 声明变量来存储数据。

  • var — 老旧的变量声明方式。一般情况下,我们不会再使用它。但是,我们会在 旧时的 "var" 章节介绍 var 和 let 的微妙差别,以防你需要它们。
  • let — 现代的变量声明方式。
  • const — 类似于 let,但是变量的值无法被修改。

变量应当以一种容易理解变量内部是什么的方式进行命名。

数据类型

  1. 有 7 种数据类型:

  2. number —— 可以是浮点数,也可以是整数,

  3. string —— 字符串类型,

  4. boolean —— 逻辑值: true/false

  5. null —— 具有单个值 null 的类型,表示”空“或“不存在”,

  6. undefined —— 一个具有单个值 undefined 的类型,表示「未分配」,

  7. 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(可以有很多个)是对象。

  • 这个方法复制了 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 有两个主要的使用场景:

  1. “隐藏” 对象属性。如果需要将属性添加到 “属于” 另一个脚本或库的对象中,则可以创建 Symbol 并将其用作属性键。Symbol 属性不出现在 for..in中,因此不会无心列出。另外,它不会被直接访问,因为另一个脚本没有我们的符号,所以它不会不小心干预它的操作。

    因此我们可以使用 Symbol 属性“秘密地”将一些东西隐藏到我们需要的对象中,但其他人不会以对象属性的形式看到它。

  2. 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" 相同,因此在实践中最后两个通常合并在一起。

转换算法是:

  1. 调用 obj[Symbol.toPrimitive](hint) 如果这个方法存在的话,
  2. 否则如果暗示是 "string"
    • 尝试 obj.toString() 和 obj.valueOf(),无论哪个存在。
  3. 否则,如果暗示 "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() );




返回列表 返回列表
评论

    分享到