常见问题
变量提升
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
- 执行全局代码,输出
4
。 - 调用
async1()
函数,输出1
。 - 在
async1()
函数中调用async2()
函数,输出3
。 - 因为
async2()
函数没有使用await
关键字,它会立即返回一个 resolved promise 对象,async1()
函数中的await
表达式得到了这个 resolved promise 对象,然后执行到下一行代码,执行了微任务队列中的所有任务,因此现在输出6
。 - 输出
8
。 async1()
函数执行完成,执行了微任务队列中的所有任务,输出2
。- 在主线程空闲时,执行 promise 的
then()
回调函数,输出7
。
在步骤 4 中,我们执行了以下操作:
async2()
函数在调用时会立即输出3
。- 因为
async2()
函数没有使用await
关键字,它会立即返回一个 resolved promise 对象。 async1()
函数中的await async2()
表达式得到了这个 resolved promise 对象,因此async1()
函数中的执行暂停,直到这个 resolved promise 对象变成 settled 状态(即 resolved 或 rejected)。- 在这个例子中,resolved promise 对象的状态已经是 resolved,因此它会立即执行队列中的微任务(也就是步骤 5 中创建的 promise 的 executor 函数)。
- 执行完微任务队列中的所有任务之后,
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
调用一个函数时,该函数被当作构造函数使用,并执行以下步骤:
- 创建一个新的空对象。
- 将这个新对象的
__proto__
链接到构造函数的prototype
属性。 - 将新对象绑定到函数调用中的
this
。 - 如果构造函数没有显式返回对象,则隐式返回这个新对象。
3. 主要区别
- 对象创建:
- 直接调用: 不创建新对象,
this
的值取决于调用时的上下文。 - 使用
new
: 创建一个新对象,this
指向这个新对象。
- 直接调用: 不创建新对象,
- 原型链:
- 直接调用: 函数不会自动设置
prototype
链。 - 使用
new
: 新对象的__proto__
属性会被设置为构造函数的prototype
属性。
- 直接调用: 函数不会自动设置
- 返回值:
- 直接调用: 函数的返回值就是函数执行的结果。
- 使用
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 事件触发之前执行。不同于异步脚本,延迟脚本保证了它们的执行顺序与它们在文档中的顺序一致。
函数执行顺序
// 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 初始化操作的理想时机,因为它能够更快地让用户看到页面的交互性,而无需等待所有资源加载完成。