1. 可变性

在 JavaScript 中有七种根本数据类型(stringnumberbooleanundefinedsymbolbigintnull),这些都是不可变的。这意味着一旦调配了一个值,咱们就无奈批改它们,咱们能够做的是将它重新分配给一个不同的值(不同的内存指针)。另一方面,其余数据类型(如 Object 和 Function)是可变的,这意味着咱们能够批改同一内存指针中的值。

// Q1let text = 'abcde'text[1] = 'z'console.log(text) // abcde

字符串是不可变的,因而一旦调配给一个值,就不能将其更改为不同的值,您能够做的是重新分配它。请记住,更改值和重新分配给另一个值是不同的。

// Q2const arr = [1, 2, 3]arr.length = 0console.log(arr) // []

调配 arr.length 为 0 与重置或革除数组雷同,因而此时数组将变为空数组。

// Q3const arr = [1, 2, 3, 4]arr[100] = undefinedconsole.log(arr, arr.length) // [1, 2, 3, 4, empty × 96, undefined] 101

因为数组占用的是间断的内存地位,所以当咱们将索引 100 赋给一个值(包含 undefined)时,JavaScript 会保留索引 0 到 索引 100 的内存,这意味着当初的数组长度为 101。

2. var 和 晋升

// Q4var variable = 10;(() => {  variable2 = 100  console.log(variable)   console.log(variable2)  variable = 20  var variable2 = 50  console.log(variable)})();console.log(variable)var variable = 30console.log(variable2)// 10 // 100 // 20 // 20 // ReferenceError: variable2 is not defined

var 是函数作用域变量,而 letconst 是块级作用域变量,只有 var 能被晋升,这意味着变量申明总是被挪动到顶部。因为晋升,您甚至能够在应用 var 关键字申明变量之前调配、调用或应用该变量。

letconst 不能被晋升,因为它启用了 TDZ(临时性死区),这意味着变量在申明之前是不可拜访的。

在下面的示例中,variable2 在函数外部申明,var 关键字使该变量仅在函数范畴内可用。所以当函数外的任何货色想要应用或者调用该变量时,referenceError 就会被抛出。

// Q5 test() // 不报错function test() {  cconsole.log('test')}test2() // 报错var test2 = () => console.log('test2')

function 关键字申明的函数能够晋升函数语句,然而不能晋升箭头函数,即便它是应用 var 进行变量申明的。

3. 必然性全局变量

// Q6 function foo() {  let a = b = 0;  a++;  return a;}foo();typeof b; // numbertypeof a; // undefinedconsole.log(a) // error: ReferenceError: a is not defined

var 是函数作用域,let 是块级作用域变量。尽管看起来 ab 都是应用 let 申明的( let a = b = 0),但实际上变量 b 被申明为全局变量并调配给 Window 对象。换句话说,它相似于:

function foo() {  window.b = 0;  let a = b;  a++;}

4. 闭包

// Q7const length = 4;const fns = [];const fns2 = [];for(var i = 0; i < length; i++) {  fns.push(() => console.log(i));}for(let i = 0; i < length; i++) {    fns2.push(() => console.log(i));}fns.forEach(fn => fn()); // 4 4 4 4fns2.forEach(fn => fn()); // 0 1 2 3

闭包是对变量环境的一种爱护,即便变量曾经更改或者已被垃圾回收。在下面的问题中,区别在于变量申明,其中第一个循环应用的是 var,第二个循环应用的是 let

var 是函数作用域变量,因而当它在 for 循环块内申明时,var 被视为全局变量而不是外部变量。另一方面,let 是块级作用域的变量,相似于 Java 和 C++ 等其余语言中的变量申明。

在这种状况下,闭包只产生在 let 变量中,推送到 fns2 数组的每个函数都会记住变量以后的值,无论变量未来是否更改。相同,fns 不记住变量的以后值,它应用全局变量的将来或最终值。

5. 对象

// Q8var obj1 = { n: 1 }var obj2 = obj1obj2.n = 2console.log(obj1) // { n: 2 }// Q9function foo(obj) {  obj.n = 3  obj.name = '测试'}foo(obj2)console,log(obj1) // { n: 3, name: '测试' }

正如咱们所知,对象变量仅蕴含该对象的内存地位指针,所以这里 obj2obj1 指向同一个对象。这意味着如果咱们更改 obj2 的任何值,obj1 也会受到影响,因为实质上它们是同一个对象。同样,当咱们在函数中将对象作为参数传递时,传递的参数只蕴含对象指针。因而,函数能够间接批改对象而不返回任何内容,这种技术称为通过援用传递

// Q10var foo = { n: 1 };var bar = foo;console.log(foo === bar); // truefoo.x = foo = { n: 2 };console.log(foo) // { n: 2 }console.log(bar) // { n: 1, x: { n: 2 } }console.log(foo === bar) // false

因为对象变量只蕴含该对象内存地位的指针,所以当咱们申明 var bar = foo 时,foobar 都指向同一个对象。

在下一个逻辑中,foo = { n: 2 } 首先运行,其中 foo 被调配给不同对象,因而 foo 有一个指向不同对象的指针。同时,foo.x = foo 正在运行,这里的 foo 依然蕴含旧指针,所以逻辑相似于:

foo = { n: 2 }bar.x = foo

所以 bar.x = { n: 2 },最初 foo 的值是 { n: 2 },而 bar{ n: 1, x: { n: 2 } }

6. this

// Q11const obj = {  name: "test",  prop: {    name: "prop name",    print: function(){       console.log(this.name)     },  },  print: function(){     console.log(this.name)   }  print2: () => console.log(this.name, this)}obj.print() // testobj.prop.print() // prop nameobj.print2() // undefined, window global object

下面的例子展现了 this 关键字在一个对象中是如何工作的,this 援用执行函数中的执行上下文对象。然而,this 范畴仅在一般函数申明中可用,在箭头函数中不可用。

下面的例子展现了显示绑定,例如在 object1.object2.object3.object4.print() 中,print 函数将应用最新的对象 object4 作为 this 上下文,如果 this 未绑定对象,它将回退到根对象,该对象是在调用 obj.print2() 时的 Window 全局对象。

另一方面,您还必须了解对象上下文之前曾经绑定的隐式绑定,因而下一个函数执行始终应用该对象作为 this 上下文。例如:当咱们应用 func.bind(<object>) 时,它将返回一个 <object> 用作新执行上下文的新函数。

7. 强制转换

// Q12console.log(1 + "2" + "2"); // 122console.log(1 + +"2" + "2"); // 32console.log(1 + -"1" + "2"); // 02console.log(+"1" + "1" + "2"); // 112console.log("A" - "B" + "2"); // NaN2console.log("A" - "B" + 2); // NaN"10,11" == [[[[10]], 11]] // true (10,11 == 10,11)"[object Object]" == { name: "test" } true

强制转换是最辣手的 JavaScript 问题之一。一般来说有两条准则,第一条是,如果 2 个操作数与 + 操作符连贯,则两个操作数将首先应用 toString 办法转变为字符串,而后连贯。同时,其余运算符(如 -*/) 会将操作数更改为数字,如果它不能被强制转换为一个数字,则返回 NAN

如果操作数蕴含一个对象或数组,那就更辣手了。任何对象的 toString 办法返回的都是 "[object Object]",但在数组中,该 toString 办法将返回由逗号分隔的根底值。

留神: == 示意容许强制转换,而 === 不容许。

8. 异步

// Q13console.log(1);new Promise(resolve => {  console.log(2);  return setTimeout(() => {       console.log(3);    resolve();  }, 0)})setTimeout(function() { console.log(4) }, 1000);setTimeout(function() { console.log(5) }, 0);console.log(6);// 1// 2// 6// 3// 5// 4

在这里,你须要晓得事件循环、宏工作和微工作队列是如何工作的。您能够在此处查看这篇文章,这里深入探讨了这些概念。个别状况下,异步函数在所用同步函数执行完后才执行。

// Q14async function foo() {  return 10;}console.log(foo()) // Promise{ <fulfilled>: 10 }

一旦函数申明为 async,它总是返回一个 Promise,无论外部逻辑是同步的还异步的。

// Q15const delay = async (item) => new Promise(    resolve => setTimeout(() => {    console.log(item);    resolve(item);  }, Math.random() * 100))console.log(1)let arr = [3, 4, 5, 6]arr.forEach(async item => await delay(item)))console.log(2)

forEach 函数总是同步的,不论每个循环是同步的还是异步的,这意味着每个循环都不会期待另一个。如果要顺次执行每个循环并互相期待,能够改用 for of

9. 函数

// Q16if(function f(){}) {  console.log(f)}// error: ReferenceError: f is not defined

在下面的例子中,if 条件被满足,因为函数申明被认为是一个真值。然而,外部块无法访问函数申明,因为它们具备不同的块作用域。

// Q17function foo() {  return   { name: 2 }}foo() // 返回 undefined

因为主动分号插入(ASI)机制,return 语句将以分号完结,并且分号上面的所有内容都不会运行。

// Q18function foo(a, b, a) { return a + b }console.log(foo(1, 2, 3)) // 3+2 = 5function foo2(a, b, c = a) { return a + b + c }console.log(foo(1, 2)) // 1+2+1 = 4function foo3(a = b, b) { return a + b }console.log(foo3(1, 2)) // 1+2 = 3 console.log(foo3(undefined, 2)) // 谬误

前三次执行的很分明,然而最初一个函数执行会报错,因为 b 在申明之前就被应用了,相似于这样:

let a = b;let b = 2;

10. 原型

// Q19function Persion() {}Persion.prototype.walk = function() {  return this}Persion.run = function() {  return this} let user = new Persion();let walk = user.walk;console.log(walk()) // window objectconsole.log(user.walk()) // user objectlet run = Persion.run;console.log(run()); // window objectconsole.log(user.run()); // TypeError: user.run is not a function

原型是存在于每个变量中的对象,用于从其父对象继承个性。例如,当您申明一个字符串变量时,该字符串变量具备一个继承自 String.prototype 的原型,这就是为什么您能够在字符串变量中调用字符串办法的起因,例如 string.replace(), string.substring() 等。

在下面的示例中,咱们将 walk 函数调配给 Persion 函数的原型,并将 run 函数调配给函数对象。这是两个不同的对象,函数应用 new 关键字创立的每个对象都将从函数原型而不是函数对象上继承办法。然而请记住,如果咱们将该函数调配给一个变量 ,如 let walk = user.walk,该函数将遗记应用 user 作为执行上下文,而是返回到 Window 对象上。

原文:https://medium.com/@andreassu...