1.什么是线程?什么是进程?有什么关系?有什么区别?
线程是CPU的基本调度单位,是程序执行的一个完整流程。进程是程序的一次执行,它占有一片独有的内存空间。
关系:一个程序至少有一个进程。一个进程中至少有一个运行的线程:主线程。一个进程中可以运行多个线程,此时程序就是多线程运行的。一个进程内的数据可以供其中的多个线程内的数据共享,但多个进程间数据是不共享的。
区别:
-
进程是资源分配最小单位,线程是程序执行的最小单位;
-
进程有自己独立的地址空间,每启动一个进程,系统都会为其分配地址空间,建立数据表来维护代码段、堆栈段和数据段,线程没有独立的地址空间,它使用相同的地址空间共享数据;
-
CPU切换一个线程比切换进程花费小;
-
创建一个线程比进程开销小;
-
线程占用的资源要⽐进程少很多。
-
线程之间通信更方便,同一个进程下,线程共享全局变量,静态变量等数据,进程之间的通信需要以通信的方式(IPC)进行;(但多线程程序处理好同步与互斥是个难点)
-
多进程程序更安全,生命力更强,一个进程死掉不会对另一个进程造成影响(源于有独立的地址空间),多线程程序更不易维护,一个线程死掉,整个进程就死掉了(因为共享地址空间);
-
进程对资源保护要求高,开销大,效率相对较低,线程资源保护要求不高,但开销小,效率高,可频繁切换;
衍生问题1 浏览器运行是单进程还是多进程?
老版本的大多数是单进程。新版本大多数是多进程。
衍生问题2 浏览器运行是单线程还是多线程?
多线程。
衍生问题3:浏览器中不同的tab页是进程还是线程?
多进程架构。由于之前的单进程多线程架构会导致这个进程creat的线程(每一个tab)有一个崩溃了会把所在的进程整个崩了(参见操作系统线程调度器内容),其他tab跟着完蛋,后来就更新为多进程架构,这在浏览器界现在已是标配,这样一个tab崩了其他的可以继续用。
2.为什么js是单线程运行的?如何验证?
var currentTime = new Date().getTime()
var timer = null
console.log('-------执行之前------');
timer = setTimeout(()=>{
console.log(new Date().getTime() - currentTime,'ms后执行');
},200)
console.log('-------执行之后------');
//一个耗时得任务
for(var i = 0; i<999999999;i++){
}
console.log('for执行完成')
运行结果:
如果是多线程,遇到定时器或者耗时任务时会开启另一个线程去执行。这样程序运行起来救不会卡顿。
衍生问题1:为什么js要用单线程而不用多线程?
JavaScript的单线程是与它的用途相关的。作为浏览器脚本语言,js的主要用途是与用户互动以及操作DOM。这就决定了它只能是单线程,否则会带来复杂的同步问题。
衍生问题2:讲讲浏览器的事件循环模型?
我们知道,当程序启动时, 一个进程被创建,同时也运行一个线程, 即为主线程,js的运行机制为单线程。js主线程是有一个执行栈的,所有的js代码都会在执行栈里运行。在执行代码过程中:
- 同步代码放在主进程中直接执行;
- 异步代码先放到异步队列:如果遇到一些异步代码(比如setTimeout,setInterval,ajax,promise.then以及用户点击等操作等),那么浏览器就会将这些代码放到一个异步进程中去等待,不阻塞主线程的执行,主线程继续执行栈中剩余的代码,当异步进程处理完毕后(比如setTimeout时间到了,ajax请求得到响应),将相应的异步任务(回调函数)放到异步队列中等待执行。
- 待同步代码执行完步,轮询执行异步队列里 的任务:当主线程执行完栈中的所有代码后,它就会检查异步队列是否有任务要执行,如果有任务要执行的话,从队列取出第一个任务队列推到主线程的执行栈中执行。如果当前任务队列为空的话,它就会一直循环查询任务队列等待任务到来。因此,叫做事件循环。
js执行引擎主要由两部分组成:
- 内存堆:这是内存分配发生的地方
- 调用栈:这是你的代码执行时的地方
事件循环可以简单描述为:
- 函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPI,接着执行同步任务,直到Stack为空;
- 在此期间WebAPIs完成这个事件,把回调函数放入CallbackQueue中等待;
- 当执行栈为空时,Event Loop把Callback Queue中的一个任务放入Stack中,回到第1步。
- Event Loop是由javascript宿主环境(像浏览器)来实现的;
- WebAPIs是由C++实现的浏览器创建的线程,处理诸如DOM事件、http请求、定时器等异步事件;
- JavaScript 的并发模型基于"事件循环";
- Callback Queue(Event Queue 或者 Message Queue) 任务队列,存放异步任务的回调函数。(队列是先进先出)
衍衍生问题1:讲讲下面代码中在事件循环模型中的过程?
var start = new Date();
setTimeout(function cb(){
console.log("时间间隔:",new Date() - start+'ms');
},500);
while(new Date()-start<1000){};
- main(Script) 函数入栈,start变量开始初始化
- setTimeout入栈,出栈,丢给WebAPIs,开始定时500ms;
- while循环入栈,开始阻塞1000ms;
- 500ms过后,WebAPIs把cb()放入任务队列,此时while循环还在栈中,cb()等待;
- 又过了500ms,while循环执行完毕从栈中弹出,main()弹出,此时栈为空,Event Loop,cb()进入栈,log()进栈,输出'时间间隔:1003ms',cb()出栈
3.下列代码打印的顺序?
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log( 'async2');
}
console.log("script start");
setTimeout(function () {
console.log("settimeout");
},0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log('script end');
在谷歌64版本下运行的答案是:
下面是流程:
自上而下执行,首先把async定义的两个异步函数放入回调队列,然后执行同步代码console.log("script start");所以先打印出
script start。然后遇到定时器,也是异步的,也把定时器放入回调队列。然后执行async1(); 因此先打印async1 start,然后遇到await async2(); 执行这一句后,输出async2后,await会让出当前线程,将后面的代码加到任务队列中,然后继续往下执行。new Promise是立即执行的,因此会打印出promise1,因为resolve()是异步函数,因此也加入到回调队列中去。然后继续往下执行,遇到同步代码console.log('script end');打印出script end。此时的任务队列(回调队列)中有setTimeout、async1的后续内容以及promse中的.then内容。由于setTimeout的优先级没有async和promise级别高(其实async和promise是一样的,因为调用async方法时就是返回一个promise对象。由于async1的后续内容比promise的.then内容更早进入回调队列,因此先打印出async1 end,再打印出promise2,最后打印出settimeout。
训练题:下面的打印顺序?
console.log(1);
setTimeout(()=>{console.log(2)})
//Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。
Promise.resolve().then(()=>{console.log(3)})
var p = new Promise((res)=>{
console.log(4)
setTimeout(()=>{
res(5)
console.log(6)
})
})
p.then((r)=>{console.log(r)})
答案是 1 4 3 2 6 5
训练题2:下列代码的打印顺序?
答案是:2 1 3 5 7 6 4 1
衍衍生问题:最后的1怎么来的?
这个和PromiseValue有关,p.finally()返回的对象的PromiseValue依赖于p的PromiseValue,可以百度一下PromiseValue,它是个promise对象的内部属性。finally内部调用的是P的then,所以打印1
为什么是7 6 4 而不是 7 4 6?(resolve(4)不是应该先进入任务队列里面吗)
6先进入的,因为resolve(6)那里创建后立马then先进入队列,而4那里是是等外面使用then之后才进入的队列
训练题3:
new Promise((resolve,reject)=>{
console.log("promise1")
resolve()
}).then(()=>{
console.log("then1-1")
new Promise((resolve,reject)=>{
console.log("promise2")
resolve()
}).then(()=>{
console.log("then2-1")
}).then(()=>{
console.log("then2-2")
})
}).then(()=>{
console.log("then1-2")
})
衍生问题1:为什么在回调函数中它们的执行优先级不同?(宏任务、微任务)
微任务 promise 、async await。遇到微任务,放在当前任务列的最底端(then或者catch里面的内容)。
宏任务 setTimeout setInterval setImmediate I/O 各种事件(比如鼠标单击事件)的回调函数。遇到宏任务,放到下一个新增任务列的最顶端。
1.全局Script代码执行完毕后,调用栈Stack会清空;
2.从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
3.继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
4.microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
5.取出宏队列microtask queue中位于队首的任务,放入Stack中执行;
6.执行完毕后,调用栈Stack为空;
总结:
1.宏队列microtask一次只从队列中取一个任务执行,执行完后就去执行微任务队列中的任务;
2.微任务队列中所有的任务都会被依次取出来执行,直道microtask queue为空;
衍生问题2:为什么要引入微任务,只有一种类型的任务不行么?
页面渲染事件,各种IO的完成事件等随时被添加到任务队列中,一直会保持先进先出的原则执行,我们不能准确地控制这些事件被添加到任务队列中的位置。但是这个时候突然有高优先级的任务需要尽快执行,那么一种类型的任务就不合适了,所以引入了微任务队列。
衍生问题3:setInterval()和setTimeout()区别?为什么不建议用setInterval?
setTimeout()只在指定时间后执行一次,而setInterval是指定时间为周期循环执行。
尽量不用setInterval()
原因一、setInterval()会无视代码错误。 setInterval有个讨厌的习惯,即对自己调用的代码是否报错这件事漠不关心。换句话说,如果setInterval执行的代码由于某种原因出了错,它还会持续不断(不管不顾)地调用该代码。
原因二、setInterval无视网络延迟假设你每隔一段时间就通过Ajax轮询一次服务器,看看有没有新数据(注意:如果你真的这么做了,那恐怕你做错了;建议使用“补偿性轮询”(backoff polling))。而由于某些原因(服务器过载、临时断网、流量剧增、用户带宽受限,等等),你的请求要花的时间远比你想象的要长。但setInterval不在乎。它仍然会按定时持续不断地触发请求,最终你的客户端网络队列会塞满Ajax调用。
原因三、setInterval不保证执行与setTimeout不同,你并不能保证到了时间间隔,代码就准能执行。如果你调用的函数需要花很长时间才能完成,那某些调用会被直接忽略。
衍生问题4:js单线程如何实现异步?(事件队列)
4.下列代码执行的结果?
var a = 6;
setTimeout(function () {
console.log(a);
},1000);
a = 66;
66。为什么setTimeout里的回调函数可以访问全局变量?因为setTimeout是浏览器自带的,是属于web API当中的宏任务,因此里面的this指向window对象。
var a = 6;
setTimeout(function () {
var a = 666;
console.log(a);
},1000);
a = 66;
666。这个不解释了。
var a = 6;
setTimeout(function () {
console.log(a);
var a = 666;
},1000);
a = 66;
undefined。在进入setTimeout()函数之后,我们得先创建一个函数,然后才能执行它,在创建函数的时候,会搜寻函数内部是否有变量创建出来了。所以,当执行流遇到alert(a)的时候,开始搜寻当前环境下有没有a变量,最终发现了一个a变量,但是在未执行var a=666之前,a并没有被赋值,所以alert(a)的最终结果为undefined。
5.讲讲let var const的差别?什么是暂时性锁区?
在ES6中,如果在代码块内使用let和const都存在暂时性锁区,都必须遵守 "先声明,后使用"的规则。
1.let声明的变量不存在变量提升的问题,必须遵循 "先声明,后使用"否则会报错。但用var声明不会出现报错。
2.let声明的变量,存在块级作用域。let声明的变量只在所声明的代码块内有效,块级作用域{},包括if和for
3.let不允许在同一作用域内重复声明同一个变量,var在同一个作用域内声明同一个变量后面会覆盖前面的,但是let会报错。
4.存在暂时性死区,在代码块内使用let声明变量之前,该变量是不可使用的。只要在作用域内使用let命令,那么所声明的变量就被"绑定"在作用域内,不管在外部有没有声明。
const与let的区别在于它是用来声明常量的。一旦声明之后不可更改。const声明的变量也只在块级作用域内生效并且存在暂时性锁区。
在代码块内,使用let、const
命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)
暂时性锁区:typeof 检测一个未被声明的变量不会报错,结果是undefdined。
setTimeout(function () {
console.log(typeof a) //undefined
var a = 666;
},1000);
衍生问题1:什么是变量提升?
变量提升是指函数作用域里面变量会覆盖外部变量。例如:
for (var i = 0; i < 10; i++) {
console.log(i);
}
console.log(i);//10
for (let i = 0; i < 10; i++) {
console.log(i);
}
console.log(i);// 报错 i is not defined
6.讲讲ES6新特性?
1、let,const,import,class变量声明语法(es5只有:var,function)
2、块级作用域
3、引入global对象
4、变量的解构赋值
5、遍历器Iterator 生成器Generator 和for...of循环遍历 通过for...of可以循环遍历Generator生成器返回的迭代器。
6、字符串、正则、数值、函数、数组、对象的扩展
7、原始数据类型Symbol
8、Set(类似数组),WeekSet、Map(类似对象),WeekMap
9、Proxy代理拦截,Reflect对象
10、异步编程解决方案:Promise对象
11、模块(module)体系
衍生问题1:什么是global对象?(谈谈global对象?)
1)所有在全局作用域内定义的属性和方法,都是Global对象的属性。
2)Global对象不能直接使用,也不能用new运算符创建。
3)Global对象在JavaScript引擎被初始化时创建,并初始化其方法和属性。
4)浏览器把Global对象作为window对象的一部分实现了,因此,所有的全局属性和函数都是window对象的属性和方法。
《JavaScript高级程序设计》中谈到,global对象可以说是ECMAScript中最特别的一个对象了,因为不管你从什么角度上看,这个对象都是不存在的。从某种意义上讲,它是一个终极的“兜底儿对象”,换句话说呢,就是不属于任何其他对象的属性和方法,最终都是它的属性和方法。 我理解为,这个global对象呢,就是整个JS的“老祖宗”,找不到归属的那些“子子孙孙”都可以到它这里来认祖归宗。所有在全局作用域中定义的属性和函数,都是global对象的属性和方法,比如isNaN()、parseInt()以及parseFloat()等,实际都是它的方法;还有就是常见的一些特殊值,如:NaN、undefined等都是它的属性,以及一些构造函数Object、Array等也都是它的方法。总之,记住一点:global对象就是“老祖宗”,所有找不到归属的就都是它的。
衍生问题2:讲一讲generator生成器?(generator与Iterator有什么关系?)
Generator 函数是 ES6 提供的一种异步编程解决方案,它是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个迭代器对象(Iterator),可以调用next
方法去遍历下一个内部状态,所以提供了一种可以暂停执行的函数。用yield来暂停。yield
表达式就是暂停标志。
衍生问题3:什么是proxy?有什么作用?
Proxy用于修改某些操作的默认行为,同等于在语言层面做出修改,所以属于一种“元编程”,即对编程语言进行编程。
Proxy(代理) 是 ES6 中新增的一个特性。Proxy 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式。
Proxy 是指代理拦截。作用是:在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
使用方法
var p = new Proxy(target, handler);
其中,target 为被代理对象。handler 是一个对象,其声明了代理 target 的一些操作。p 是代理后的对象。
当外界每次对 p 进行操作时,就会执行 handler 对象上的一些方法。handler 能代理的一些常用的方法如下:
-
get:读取
-
set:修改
-
has:判断对象是否有该属性
-
construct:构造函数
看如下的demo:
var target = {
name: 'obj'
};
var logHandler = {
get: function(target, key) {
console.log(`${key} 被读取`);
return target[key];
},
set: function(target, key, value) {
console.log(`${key} 被设置为 ${value}`);
target[key] = value;
}
}
var targetWithLog = new Proxy(target, logHandler);
targetWithLog.name; // 控制台输出:name 被读取
targetWithLog.name = 'others'; // 控制台输出:name 被设置为 others
console.log(target.name); // 控制台输出: others
在上面的 demo 中:
-
targetWithLog 读取属性的值时,实际上执行的是 logHandler.get :在控制台输出信息,并且读取被代理对象 target 的属性。
-
在 targetWithLog 设置属性值时,实际上执行的是 logHandler.set :在控制台输出信息,并且设置被代理对象 target 的属性的值。
衍生问题2.2:什么是reflect?有什么作用?
Reflect词意为“反射”,其对象方法与Proxy对象的方法对应,并且也与Object的方法对应,也就是javaScript用来实现映射的API,注意Reflect不能执行new指令。
详细看:https://es6.ruanyifeng.com/?search=reflect&x=0&y=0#docs/reflect
7.讲讲模块化?
1.模块化就是为了减少系统耦合度,提高高内聚,减少资源循环依赖,增强系统框架设计。
2.让开发者便于维护,同时也让逻辑相同的部分可复用。
3.块的内部数据与实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信。
衍生问题1:讲讲模块化规范?
CommonJS
CommonJS
是一种使用广泛的JavaScript
模块化规范,核心思想是通过require
方法来同步的加载依赖的其他模块,通过module.exports
导出需要暴露的接口,node
的模块化就是通过CommonJS
实现的。
一,定义模块:
根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在该模块内部定义的变量,无法被其他模块读取,除非定义为global对象的属性。
二,模块输出:
模块只有一个出口,module.exports对象,我们需要把模块希望输出的内容放入该对象。
三,加载模块:
加载模块使用require方法,该方法读取一个文件并执行,返回文件内部的module.exports对象。
// example.js
var x = 5;
var addX = function (value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
//引入
var example = require('./example.js');//如果参数字符串以“./”开头,则表示加载的是一个位于相对路径
console.log(example.x); // 5
console.log(example.addX(1)); // 6
8.ES6模块和 CommonJS 模块有哪些差异?
1.CommonJS
模块是运行时加载,ES6
模块是编译时输出接口。
2.ES6
模块在编译时,就能确定模块的依赖关系,以及输入和输出的变量。ES6
模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。CommonJS
加载的是一个对象,该对象只有在脚本运行完才会生成
3.CommonJS
模块输出的是一个值的拷贝,ES6
模块输出的是值的引用。
也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。如:
//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
module.exports = {
name
};
//index.js
const name = require('./name');
console.log(name); //William
setTimeout(() => console.log(name), 300); //William
但是当输出的是复杂数据类型时,实际上拷贝的是栈内存中的地址,所以输出的值会发生改变:
// name.js
let name = 'William';
let hobbies = ['coding'];
setTimeout(() => {
name = 'Yvette';
hobbies.push('reading');
}, 300);
module.exports = { name, hobbies };
// index.js
const { name, hobbies } = require('./name');
console.log(name); // William
console.log(hobbies); // ['coding']
setTimeout(() => {
console.log(name); // William
console.log(hobbies); // ['coding', 'reading']
}, 500);
ES6 模块的运行机制与 CommonJS
不一样。JS
引擎对脚本静态分析的时候,遇到模块加载命令 import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
//name.js
var name = 'William';
setTimeout(() => name = 'Yvette', 200);
export { name };
//index.js
import { name } from './name';
console.log(name); //William
setTimeout(() => console.log(name), 300); //Yvette
ES6模块使用import关键字导入模块,export关键字导出模块:
/** 导出模块的方式 **/
var a = 0;
export { a }; //第一种
export const b = 1; //第二种
let c = 2;
export default { c }//第三种
let d = 2;
export default { d as e }//第四种,别名
/** 导入模块的方式 **/
import { a } from './a.js' //针对export导出方式,.js后缀可省略
import main from './c' //针对export default导出方式,使用时用 main.c
import 'lodash' //仅仅执行lodash模块,但是不输入任何值
export default 命令
从前面的例子可以看出,使用 import 命令加载模块时需要知道变量名或者函数名,或者整个文件,否则无法加载。为了方便,可以使用 export default 命令为模块指定默认输出,加载该模块时,可以使用 import 命令为其指定任意名字。
// 定义模块 math.js
let basicNum = 0
let add = function(a, b) {
return a+b
}
export default { basicNum, add }
// 引入
import math from './math'
function test() {
console.log(math.add(99 + math.basicNum))
}
现在前端框架基本上使用 ES6 的模块化语法,node.js 仍然保持 require 导入,两者最主要的区别是:
-
require 是运行时加载,import 是编译时加载
即下面的条件加载时不可能实现的
if (x === 2) {
import MyModual from './myModual'
}
-
ES6
模块自动采用严格模式,无论模块头部是否写了"use strict"
; -
require
可以做动态加载,import
语句做不到,import
语句必须位于顶层作用域中。 -
ES6
模块的输入变量是只读的,不能对其进行重新赋值 -
当使用
require
命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS
模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
JS的模块化方案,CommonJS, ES6 Modle区别?
答案:
1.commonjs使用require导入模块,es6使用import导入模块。require可以做动态加载,但是import不行,import
语句必须位于顶层作用域中。require 是运行时加载,import 是编译时加载。
2.CommonJS
导出的是一个对象,该对象只有在脚本运行完才会生成。ES6
模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。ES6
模块在编译时,就能确定模块的依赖关系,以及输入和输出的变量。
3.CommonJS
模块输出的是一个值的拷贝,ES6
模块输出的是值的引用。
9.模拟实现 new 操作符?
MDN对new
运算符的定义:new
运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
new实现了那些功能:
-
创建了一个空对象
-
空对象的原型指向了构造函数的原型
-
让this指向新创建的空对象,并且执行对象的主体(为这个新对象添加属性)
-
判断返回值的类型,如果是值类型就返回新创建的对象,如果是引用类型,就返回这个引用类型的对象
-
如果函数没有返回对象类型Object(包括Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用将返回该对象的引用
function copyNew(obj,...args){
//1.创建了一个空对象
const newObj = {}
//2. 空对象的原型指向了构造函数的prototype
newObj.__proto__ = obj.prototype
//上面的两步可以合为一步
// let newObj = Object.create(obj.prototype)
//3. 将obj的this改为新创建对象
let result = obj.apply(newObj,args)
//判断类里面有返回值吗?返回值是对象吗?如果是的那那就返回类中的返回值,如果不是的话那就返回新创建的对象
return typeof result === 'object' ? result : newObj
}
10.Object.create(null) 与 new Object({}) 有什么区别?
MDN上的定义: Object.create(proto,[propertiesObject])
-
proto:新创建对象的原型对象
-
propertiesObject:可选。要添加到新对象的可枚举(新添加的属性是其自身的属性,而不是其原型链上的属性)的属性。
看代码:
var Base = function () {
this.a = 2
}
var o1 = new Base();
var o2 = Object.create(Base);
console.log(o1.a); //2
console.log(o2.a); //undefined
var Base = function () {
this.a = 2
}
Base.prototype.a = 3;
var o1 = new Base();
var o2 = Object.create(Base.prototype); //新创建对象的原型对象
var o3 = Object.create(Base);
console.log(o1.a); //2
console.log(o2.a); //3
console.log(o2.__proto__.a); //3
console.log(o3.a); // undefined
(F在创建后被销毁)
看完上图,我们就知道了,为什么通过Object.create构造的连Base原型上的属性都访问不到,因为他压根就没有指向他的prototype。这也就说明了__proto__
和 prototype
的区别。所以上面在prototype定义的a,只是Base的prototype对象上的一个属性。
再来看看就是:
-
new关键字必须是以function定义的。
-
Object.create 则 function和object都可以进行构建。
小结:
因此,该问题的答案是Object.create(null)创建出来的空对象没有Object构造函数里的方法,例如toString,hasownProperty等。
衍生问题1: Object.create(null)的使用场景?
Object.create(null)的使用场景:(Vue与Vuex的源码中,作者都使用了Object.create(null)
来初始化一个新对象。)
-
你需要一个非常干净且高度可定制的对象当作数据字典的时候;
-
想节省
hasOwnProperty
带来的一丢丢性能损失并且可以偷懒少些一点代码的时候
衍生问题2:Object.create怎么实现?
Object.create = function (o) {
var F = function () {};
F.prototype = o;
return new F();
};
11.promise内部有几种状态?
一个promise的状态只有三种:等待态Pending、执行态Fulfilled和拒绝态Rejected。
-
处于等待态时,promise可以迁移到执行态或者拒绝态。
-
处于执行态时,promise不能迁移到其他任何状态,且必须拥有一个不可变的终值。
-
处于拒绝态时,promise不能迁移到其他任何状态,且必须拥有一个不可变的据因。
12.promise是用来干嘛的?讲讲promise原理?
Promise 是异步编程的一种解决方案:从语法上讲,promise是一个对象,从它可以获取异步操作的消息;它的原理是:通过new Promise传入一个函数,在这个函数运行的时候去修改Promise内部的状态。然后通过then注册的回调函数根据内部状态来去执行相应的方法。
基本过程:
-
初始化 Promise 状态(pending)
-
执行fn函数,修改Promise内部的状态
-
执行 then(..) 注册回调处理数组(then 方法可被同一个 promise 调用多次)
衍生问题1:如果让你自己实现promise,如何实现?
1.加入状态机制,通过三个状态来管理回调函数。
2.要有成功和失败的回调方法,在调用成功时,就返回成功态,调用失败时,返回失败态。
3.then方法要能实现链式调用。在promise中,要实现链式调用返回的结果是返回一个新的promise,第一次then中返回的结果,无论是成功或失败,都将返回到下一次then中的成功态中,但在第一次then中如果抛出异常错误,则将返回到下一次then中的失败态中
4.要保证执行顺序。对于异步代码要进行相应的处理。
想要看从零开始手写promiseA+的可以看我这篇博文:https://blog.csdn.net/weixin_42292991/article/details/108564683
手写promise也是一个常考点,如果能写出来整个promise则是一个很大的加分项。花个一两天来手敲几遍就能手写了。
衍生问题2:promise里面new Error(),用try catch可以捕获吗?
答案:不可以。try catch只能捕获到同步代码,
window.onerror = function () {
console.log('window err');
}
try{
console.log(x) // 这里 x 未定义
}catch(err){
console.log('try err')
}
//这里打印出 try err 捕获到了
window.onerror = function () {
console.log('window err');
}
// 异步,宏任务
try{
setTimeout(function(){
console.log(x) // 这里 x 未定义
},10)
}catch(err){
console.log('try err') // 这里是不会执行的
}
//window err 然后红色报错 也就是捕获不到
promise 对象里面同步代码抛出的错误在没有通过 promise 的 catch 方法捕获时是会打印报错的(不会阻止 promise 外面代码的执行),但是不会传递到外面触发其他错误监听函数(比如 window.onerror 、try-catch 等)
window.onerror = function () { // 我们添加了 window 的 onerror 处理函数
console.log('window err')
}
promise = new Promise(function(resolve, reject) {
throw new Error('test');
});
promise.then(function(value) { console.log(value) })
// 控制台输出:
// Uncaught (in promise) Error: test 红色报错
但是如果是在异步代码里面抛出错误
window.onerror = function () {
console.log('window err')
}
var promise = new Promise(function (resolve, reject) {
setTimeout(function () {
throw new Error('test')
}, 0)
resolve('ok');
});
promise
.then(function (value) { console.log(value) })
.catch(() => console.log('promise catch err'))
// 控制台输出:
// ok
// window err
// Uncaught Error: test
这里由于是在 setTimeout
里面抛出错误的,所以报错会在同步代码执行完后的下一轮 “事件循环” 里执行,也就是说当 setTimeout
里面的函数执行后报错时,promise 已经执行完了(所以就算 resolve('ok')
写在 setTimeout
下面也是先输出 ok),所以这个错误是在 Promise 函数体外抛出的,当然也就不会被 promise 的 catch 方法捕获,所以就会传到 window 上被捕获并输出 window err
,然后再被浏览器捕获输出Uncaught Error: test
,如果在 window onerror 处理程序里面 return true,就不会看到浏览器捕获输出的 Uncaught Error: test
报错。
// 异步,微任务
try {
new Promise(() => {
throw new Error('new promise throw error');
});
} catch (error) {
console.log(error);
}
解释
try-catch 主要用于捕获异常,注意,这里的异常,是指同步函数的异常,如果 try 里面的异步方法出现了异常,此时catch 是无法捕获到异常的,原因是因为:当异步函数抛出异常时,对于宏任务而言,执行函数时已经将该函数推入栈,此时并不在 try-catch 所在的栈,所以 try-catch 并不能捕获到错误。对于微任务而言,比如 promise,promise 的构造函数的异常只能被自带的 reject 也就是.catch 函数捕获到。
衍生问题3:用promise实现sleep函数?
function sleep(t=1000) {
return new Promise(resolve => {
setTimeout(() => {
resolve(t)
}, t)
})
}
console.log('开始');
sleep(2000).then((t)=>{
console.log(t+'ms');
})
13.面向对象三种特性?分别解释一下
面向对象的三个基本特征是:封装、继承、多态。
封装是将复杂的功能封装起来,对外开放一个接口,简单调用即可。
将描述事物的数据和操作封装在一起,形成一个类;被封装的数据和操作只有通过提供的公共方法才能被外界访问(封装隐藏了对象的属性和实施细节),私有属性和方法是无法被访问的,表现了封装的隐藏性,增加数据的安全性。
面向对象编程 (OOP) 语言的一个主要功能就是“继承”。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。
多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。通俗来说: 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果。
多态的实现案例:
/**
* 多态的实现案例
* @param animal
*/
var makeSound = function (animal) {
animal.sound();
}
var Duck=function () {}
var Dog=function () {}
Duck.prototype.sound=function () {
console.log("嘎嘎嘎")
}
Dog.prototype.sound=function () {
console.log("旺旺旺")
}
makeSound(new Duck());
makeSound(new Dog());
14.类和接口有什么区别?
不同点:
接口不能直接实例化。
接口不包含具体方法的实现。
接口可以实现多继承,类只能单继承。 接口的主要目的就是为了实现多态。
类定义可以在不同的源文件之间进行拆分。(通过关键字 partial 可以把类放在不同文件。 比如文件名为A.cs,里面定义的类如下 public partial class A{}。然后另一个文件名A1.cs,里面同样 public partial class A{} 。这样的两个文件定义的是一个A类。(前提是这两个文件在同一个类库,namespace 也相同))
相同点:
接口、类和结构都可以从多个接口继承。
接口类似于抽象基类:继承接口的任何非抽象类型都必须实现接口的所有成员。
接口和类都可以包含事件、索引器、属性。