今日一问:
下面的JS代码执行后,最终输出的结果是什么?
function Person(name) {
this.name = name;
this.getName = function() {
return this.name;
};
}
const person1 = new Person('Alice');
const person2 = { name: 'Bob' };
person2.getName = person1.getName;
console.log(person2.getName());
答案和解析可在文章底部查看。
今日面试题:
1、JS中调用构造函数时,new操作符具体做了哪些操作?
① 创建新的空对象
JS引擎首先会创建一个新的空对象,作为构造函数创建的实例。这个对象的类型是由构造函数决定的。
② 设置新对象的原型属性
新对象的内部原型[[Prototype]]
(浏览器环境中通过__proto__
属性访问)属性会被设置为构造函数的prototype
属性的值。这意味着新对象可以访问到构造函数原型上的属性和方法。
③ 绑定 this 关键字
在构造函数内部,将this
关键字指向新对象,从而可以通过 操作this
向新对象添加属性和方法。
④ 执行构造函数
执行构造函数中的代码,初始化新对象的属性和方法,包括赋值语句、函数调用等等。
⑤ 返回对象
如果构造函数中显式的返回了一个对象类型(Object
,包含对象、数组等,以及一种特殊的对象类型-函数)的值,则将该值作为new
操作符的最终结果;如果构造函数显式的返回了一个非对象类型的(字符串、数字等)值或者干脆没有返回值,则会将新对象作为new
操作符的最终结果。
案例代码:
function Person(name) {
this.name = name;
// 返回了一个非对象类型的值
return '30';
}
// p的值是一个有name属性的Person对象
let p = new Person("John");
function Person2(name) {
this.name = name;
// 返回了一个对象类型的值
return {age: 30};
}
// p2的值是{age: 30},而不是一个有name属性的Person2对象
let p2 = new Person2("John");
2、请用JS模拟 new 操作符的实现
// 一个构造函数
function Person(name) {
this.name = name;
this.sayHello = function() {
console.log(`Hello, my name is ${this.name}`);
};
}
// 模拟new操作符的实现
// constructor-表示构造函数 args-表示要传递的参数
function myNew(constructor, ...args) {
// 1、创建一个新的空对象
const obj = {};
// 2、设置新对象的原型属性
Object.setPrototypeOf(obj, constructor.prototype);
// 3、绑定 this 关键字 并执行构造函数
const res = constructor.apply(obj, args);
// 4、根据构造函数的返回值 决定new操作的返回值
// 如果构造函数的返回值是对象类型 则返回该对象 否则返回创建的新对象
if(res!==null && (typeof res==="object" || typeof res==="function")) {
return res;
} else {
return obj;
}
}
// 测试自定义的new函数
const p = myNew(Person, 'Bob');
p.sayHello(); // 输出:Hello, my name is Bob
3、前端异步加载JS的方法有哪些?
① async
在<script>
标签中添加async
属性,浏览器会在解析HTML文档的同时异步下载引用的JS文件,并在文件下载完成后,立即执行。
<script async src="***/***/***/test.js"></script>
② defer
在<script>
标签中添加defer
属性,浏览器会在解析HTML文档的同时异步下载引用的JS文件,文件下载完成后,会继续等待DOM解析完成,再执行。
<script defer src="***/***/test.js"></script>
③ 动态创建脚本元素
在JS中,通过在特定的时机动态创建<script>
元素,设置src
属性引用JS文件,并添加到页面中,此操作类似于设置async
属性,浏览器会在解析HTML文档的同时异步下载引用的JS文件,并在文件下载完成后,立即执行。
<script>
// 创建异步脚本
const script = document.createElement("script");
script.src = "***/***/test.js";
document.body.appendChild(script);
</script>
④ XMLHttpRequest API 或 fetch API
通过XMLHttpRequest
或fetch
发起ajax请求,异步获取JS脚本内容,然后通过eval()
或动态函数来执行脚本内容。
注: eval()
存在很大的安全风险,更推荐通过动态函数来执行脚本。
<script>
// 发起fetch请求
fetch('***/***/test.js')
.then(response => response.text())
.then(code => {
// 不推荐
// eval(code);
// 推荐
const scriptFunction = new Function(code);
scriptFunction();
});
</script>
⑤ import 动态导入
在ES2020中,引入了import()
动态导入的功能,它会返回一个Promise
。当遇到import()
导入JS的语句时,浏览器会异步的加载指定的JS脚本,并在加载和解析完成后,将结果传递给then
方法中的回调函数。
<script>
import('***/***/test.js').then(module => {
// 使用导入的模块
module.myFunction();
}).catch(err => {
console.error('模块加载失败:', err);
});
</script>
⑥ 模块化加载
使用ES6的模块化,在 <script>
标签中添加 type="module"
属性,浏览器会在解析HTML文档的同时异步下载引用的JS文件,并且会等待DOM解析完成后再执行,类似于defer
属性。
在模块内部,如果存在import
语句来导入其他模块,浏览器会同样以异步方式去加载这些依赖模块。这个过程是递归的,浏览器会先解析和加载最底层的依赖模块,然后逐步向上执行。只有当一个模块的所有依赖都加载完成后,这个模块才会被执行。
<script type="module" src="***/***/test.js"></script>
⑦ Web Worker
严格来说,Web Worker 本身不是专门用于异步加载 JavaScript 的技术,但可以通过它来间接实现类似的效果。主线程先创建一个 Web Worker,然后让它在浏览器后台线程中加载和执行JS脚本,不会阻塞主线程。但是Web Worker 无法直接操作DOM,需要借助消息传递机制来间接操作DOM。
今日一问答案:‘Bob’
解析:
这是一道考察JS中this
指向的问题,我在之前的文章中仔细讲过相关内容,感兴趣的同学可以去翻翻看一下。
代码结构很简单,首先声明了一个Person
的构造函数,包含一个name
属性和一个getName
方法,getName
方法中返回了this.name
的值。然后通过new
构造了一个实例对象person1
,并设置name
属性值为Alice
。又通过字面量创建了一个对象person2
,并且也设置一个name
属性,属性值为Bob
。再然后给person2
对象设置一个getName
方法,使其指向person1
的getName
方法。最后调用person2.getName()
,问输出结果是什么。
对于this
指向这一类问题,简单来说,遵循下面的原则即可:this
的指向取决于函数的调用者(箭头函数除外)。在当前代码中,getName()
方法是一个普通函数(非箭头函数),因此虽然person2
的getName()
方法指向的是person1
的getName()
方法,但最终函数的调用者,是person2
对象。所以函数中的 this
指向person2
,this.name
访问到的是person2.name
,也就是Bob
。最终,输出的结果为Bob
。