跳到主要内容

常见问题

变量提升

var foo = function () {
console.log('foo1');
};
foo();

var foo = function () {
console.log('foo2');
};
foo();

function foo() {
console.log('foo1');
}
foo();

function foo() {
console.log('foo2');
}
foo();
// foo1 foo2 foo2 foo2
var b = 10;
(function b() {
b = 20;
console.log(b); // function b() { ... }
})();

这是因为代码中定义了一个自执行的函数,这个函数的名称是 b。在函数内部,有一行代码尝试将 b 变量赋值为 20,但这个赋值语句会失败。

原因是,函数名 b 在自执行函数内部被重新定义为函数本身,这会导致 b 成为一个指向该函数的引用。因此,尝试将 b 变量赋值为 20 实际上是尝试修改函数的引用,而不是创建一个新的局部变量 b

所以,当 console.log(b) 被执行时,它实际上在打印函数对象本身,而不是变量 b 的值。因此,输出的内容是函数对象的字符串表示:ƒ b() { b = 20; console.log(b); }

这段代码中涉及了函数作用域和变量提升的概念。在函数内部,函数名 b 会覆盖外部作用域的变量 b,并且在函数内部,b 被定义为函数本身。这是 JavaScript 的特性之一,因此要小心不要在函数内部重新定义已存在于外部作用域的变量,以避免混淆和错误。

== 和 ===

const obj3 = { a: 1 };
const obj4 = { b: 2 };
console.log(obj3 == obj4); // false
console.log(obj3 === obj4); // false

==(等于运算符):

  • 用于比较两个值是否相等。
  • 在比较时会进行类型转换,尝试将两个值转换为相同的类型,然后再比较它们。
  • 如果两个值的类型不同,JavaScript 会尝试将它们转换为相同的类型,然后再进行比较。
  • 例如,'5' == 5 会返回 true,因为它们被转换为相同的数字类型并且值相等。

===(全等运算符):

  • 用于比较两个值是否严格相等,包括值和类型都必须相等。
  • 不进行类型转换,只有在值和类型都相同的情况下才会返回 true。
  • 例如,'5' === 5 会返回 false,因为它们的类型不同。

词法作用域

词法作用域在函数声明时(编译)就已经被决定。

// 箭头函数不会创建自己的 this,它会捕获其定义时所在的上下文中的 this 值。
// 因此,它的 this 值在函数定义时就已经确定了,而不是在函数调用时确定的。
const o1 = {
name: 'My Object',
regularFunction: function () {
console.log(this.name);
},
arrowFunction: () => {
console.log(this.name);
}
};

const o2 = {
name: 2,
age: o1.arrowFunction()
};

async 执行机制

async function async1() {
console.log('1');
await async2();
console.log('2');
}
async function async2() {
console.log('3');
}

console.log('4');

setTimeout(function () {
// 推到宏队列
console.log('5');
}, 0);

async1(); // 调用方法,执行函数

new Promise(function (resolve) {
console.log('6');
resolve();
}).then(function () {
console.log('7');
});
console.log('8');
// 4 1 3 6 8 2 7 5
  1. 执行全局代码,输出 4
  2. 调用 async1() 函数,输出 1
  3. async1() 函数中调用 async2() 函数,输出 3
  4. 因为 async2() 函数没有使用 await 关键字,它会立即返回一个 resolved promise 对象,async1() 函数中的 await 表达式得到了这个 resolved promise 对象,然后执行到下一行代码,执行了微任务队列中的所有任务,因此现在输出 6
  5. 输出 8
  6. async1() 函数执行完成,执行了微任务队列中的所有任务,输出 2
  7. 在主线程空闲时,执行 promise 的 then() 回调函数,输出 7

在步骤 4 中,我们执行了以下操作:

  1. async2() 函数在调用时会立即输出 3
  2. 因为 async2() 函数没有使用 await 关键字,它会立即返回一个 resolved promise 对象。
  3. async1() 函数中的 await async2() 表达式得到了这个 resolved promise 对象,因此 async1() 函数中的执行暂停,直到这个 resolved promise 对象变成 settled 状态(即 resolved 或 rejected)。
  4. 在这个例子中,resolved promise 对象的状态已经是 resolved,因此它会立即执行队列中的微任务(也就是步骤 5 中创建的 promise 的 executor 函数)。
  5. 执行完微任务队列中的所有任务之后,async1() 函数才会继续执行到下一行代码,输出 2

总之,步骤 4 中的关键点是,因为 async2() 函数立即返回一个 resolved promise 对象,它不会阻塞 async1() 函数的执行,而是将控制权交回给 async1() 函数,让它继续执行,直到下一个 await 表达式或函数结束。在这个例子中,async1() 函数在得到 resolved promise 对象之后,立即执行了队列中的微任务,然后才输出 2

函数执行顺序

// 构造函数
function Foo() {
// 赋值给全局变量 getName 的匿名函数
// 如果 Foo 函数没有执行,下面这个 getName 的赋值行为是不进行的
getName = function () {
console.log(1);
};
console.log(this);
return this;
}

// 函数Foo上的静态方法,一个函数对象上的方法、属性
Foo.getName = function () {
console.log(2);
};

// 扩展函数原型上的方法
// 调用 new Foo().getName()
Foo.prototype.getName = function () {
console.log(3);
};

// 给全局变量赋值一个匿名函数
var getNanme = function () {
console.log(4);
};

// 函数声明
function getName() {
console.log(5);
}

// 注意 new 函数时,this指向函数生成的实例
Foo.getName(); // 2 执行静态方法
getName(); // 5 -> 4 this => window 函数表达式 和 函数声明 变量提升 先时函数声明的5,后被函数表达式赋给4
Foo().getName(); // 5 => 1 Foo()调用,将释放内部全局函数,此时,z覆盖 之前的函数声明,然后执行被释放出来的全局函数表达式 此时 this 指向 window
getName(); // 1 同上,这是最后的函数表达式
new Foo.getName(); // 2 执行静态方法
new Foo().getName(); // 3 执行原型上的方法 此时 this 指向 foo
new new Foo().getName(); // 3 执行原型上的方法 此时 this 指向 foo

new 和调用函数的区别

1. 直接调用函数

当直接调用一个函数时,它只是普通的函数执行,this 的指向取决于调用时的上下文。

2. 使用 new 调用函数

当使用 new 调用一个函数时,该函数被当作构造函数使用,并执行以下步骤:

  1. 创建一个新的空对象。
  2. 将这个新对象的 __proto__ 链接到构造函数的 prototype 属性。
  3. 将新对象绑定到函数调用中的 this
  4. 如果构造函数没有显式返回对象,则隐式返回这个新对象。

3. 主要区别

  1. 对象创建:
    • 直接调用: 不创建新对象,this 的值取决于调用时的上下文。
    • 使用 new: 创建一个新对象,this 指向这个新对象。
  2. 原型链:
    • 直接调用: 函数不会自动设置 prototype 链。
    • 使用 new: 新对象的 __proto__ 属性会被设置为构造函数的 prototype 属性。
  3. 返回值:
    • 直接调用: 函数的返回值就是函数执行的结果。
    • 使用 new: 如果构造函数显式返回一个对象,则返回该对象;否则返回新创建的对象。
function Test() {
console.log(this);
this.name = 'Test';
return function () {
return true;
};
}
var test = new Test(); // this => Test {}
console.log(test); // ƒ () {return true}
var test1 = Test(); // this => window
console.log(test1); // ƒ () {return true}

上面这个函数,使用 new 执行以后,并没有返回一个实例对象,而是返回了 return 之后的函数;直接调用函数,不会改变 this 的指向,如果是返回一个简单数据类型呢:

function Test() {
console.log(this);
this.name = 'Test';
return '这是一个字符串';
}
var test = new Test(); // this => Test{}
console.log(test); // Test {name: "Test"}
var test1 = Test(); // this => window
console.log(test1); // ƒ () {return true}

这时会返回 Test 的实例对象。

通过上面两段代码,我们可以得出一个猜测,如果函数返回值为简单数据类型时,new 函数将会返回一个该函数的实例对象,而如果函数返回一个引用类型,则 new 函数与直接调用函数产生的结果等同。

4. 总结

  • 直接调用: 只执行函数本身,不会创建新对象,this 取决于调用上下文。
  • 使用 new: 创建新对象,设置原型链,将 this 绑定到新对象,并返回新对象(除非显式返回其他对象)。

通常,为了区分普通函数和构造函数,构造函数的首字母要大写。

函数的 this 指向问题

// 默认绑定
function girl1() {
console.log(this); // window
}
girl1();

// 隐式绑定
var girl2 = {
name: '小红',
age: 18,
detail: function () {
console.log(this); // 指向 girl2 对象
console.log('姓名: ', this.name);
console.log('年级: ', this.age);
}
};
girl2.detail();

// 硬绑定
var girlName = {
name: '小红',
sanName: function () {
console.log(this); // 指向 call,apply 的对象
console.log('我的女孩:', this.name);
}
};
var girl3 = {
name: '小白'
};
var girl4 = {
name: '小黄'
};
girlName.sanName.call(girl3);
girlName.sanName.call(girl4);

// 构造函数绑定
function Lover(name) {
this.name = name;
this.sayName = function () {
console.log(this); // 指向调用构造函数生成的实例
console.log('我的女孩:', this.name);
};
}
var name = '小白';
var xiaoHong = new Lover('小红');
xiaoHong.sayName();

函数的参数问题

// 情景一:
// 情景二:加上"use strict"
function foo(x, y, z) {
console.log(arguments.length);
console.log(arguments[0]);
arguments[0] = 10;
console.log(x);
arguments[z] = 100;
console.log(z);
}
foo(1, 2);
console.log(foo.length); // 3
console.log(foo.name); // foo

// 情景一: 2 1 10 undefined 3 foo
// 情景二:加上"use strict" 2 1 1 undefined 3 foo

// use strict对arguments做了以下限定
// 不允许对arguments赋值。禁止使用arguments.callee。arguments不再追踪参数的变化

async 和 defer

  • 异步脚本(async): 该脚本会异步加载并立即执行,而不会阻塞页面的解析和渲染。异步脚本的执行顺序不受页面中其它元素的影响。
  • 延迟脚本(defer): 该脚本会在文档解析完成后,DOMContentLoaded 事件触发之前执行。不同于异步脚本,延迟脚本保证了它们的执行顺序与它们在文档中的顺序一致。

async和defer

函数执行顺序

// 58 数科
// 2 3 4 6 5 1
function test() {
setTimeout(() => {
console.log(1);
}, 0);
new Promise(res => {
console.log(2);
for (let i = 0; i < 1000; i++) {
i == 99 && res();
i == 999 && console.log(3);
}
console.log(4);
}).then(() => {
console.log(5);
});

console.log(6);
}
test();

DOMContentLoaded 和 window.onload

  • DOMContentLoaded 是浏览器中一个非常有用的事件,它表示当初始 HTML 文档已经完全加载和解析,而无需等待样式表、图片和子框架的加载时触发。换句话说,当文档的 DOM(Document Object Model)已经构建完成,但是外部资源(如样式表和图片)可能尚未完全加载时,DOMContentLoaded 事件就会被触发。
  • window.onload(): 这是一个标准的事件处理函数,用于在整个窗口和所有资源加载完成后执行。当整个文档,包括其所有资源(例如图片和样式表)都已加载时,触发该事件。

通常情况下,DOMContentLoaded 事件比 window.onload 事件更早触发,因为它不需要等待所有的外部资源加载完毕。这使得它成为执行一些 JavaScript 初始化操作的理想时机,因为它能够更快地让用户看到页面的交互性,而无需等待所有资源加载完成。