6

image-20230207094547683.png

1. 可变性

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

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

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

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

分配 arr.length 为 0 与重置或清除数组相同,因此此时数组将变为空数组。

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

因为数组占用的是连续的内存位置,所以当我们将索引 100 赋给一个值(包括 undefined)时,JavaScript 会保留索引 0 到 索引 100 的内存,这意味着现在的数组长度为 101。

2. var 和 提升

// Q4
var variable = 10;
(() => {
  variable2 = 100
  console.log(variable) 
  console.log(variable2)
  variable = 20
  var variable2 = 50
  console.log(variable)
})();
console.log(variable)
var variable = 30
console.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; // number
typeof a; // undefined

console.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. 闭包

// Q7
const 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 4
fns2.forEach(fn => fn()); // 0 1 2 3

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

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

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

5. 对象

// Q8
var obj1 = { n: 1 }
var obj2 = obj1
obj2.n = 2
console.log(obj1) // { n: 2 }

// Q9
function foo(obj) {
  obj.n = 3
  obj.name = '测试'
}
foo(obj2)
console,log(obj1) // { n: 3, name: '测试' }

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

// Q10
var foo = { n: 1 };
var bar = foo;
console.log(foo === bar); // true
foo.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

// Q11
const 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() // test
obj.prop.print() // prop name
obj.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. 强制转换

// Q12
console.log(1 + "2" + "2"); // 122
console.log(1 + +"2" + "2"); // 32
console.log(1 + -"1" + "2"); // 02
console.log(+"1" + "1" + "2"); // 112
console.log("A" - "B" + "2"); // NaN2
console.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. 异步

// Q13
console.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

在这里,你需要知道事件循环、宏任务和微任务队列是如何工作的。您可以在此处查看这篇文章,这里深入探讨了这些概念。一般情况下,异步函数在所用同步函数执行完后才执行。

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

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

// Q15
const 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. 函数

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

在上面的例子中,if 条件被满足,因为函数声明被认为是一个真值。但是,内部块无法访问函数声明,因为它们具有不同的块作用域。

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

由于自动分号插入(ASI)机制,return 语句将以分号结束,并且分号下面的所有内容都不会运行。

// Q18
function foo(a, b, a) { return a + b }
console.log(foo(1, 2, 3)) // 3+2 = 5

function foo2(a, b, c = a) { return a + b + c }
console.log(foo(1, 2)) // 1+2+1 = 4

function 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. 原型

// Q19
function Persion() {}
Persion.prototype.walk = function() {
  return this
}
Persion.run = function() {
  return this
} 

let user = new Persion();
let walk = user.walk;
console.log(walk()) // window object
console.log(user.walk()) // user object
let run = Persion.run;
console.log(run()); // window object
console.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...


破晓L
2.1k 声望3.6k 粉丝

智慧之子 总以智慧为是