1. {}和[]的valueOf和toString的结果是什么?
{} 的 valueOf 结果为 {} ,toString 的结果为 "[object Object]"
[] 的 valueOf 结果为 [] ,toString 的结果为 ""
也就是说:{} 和[]valueOf 结果为是他们本身
JavaScript的许多内置对象都重写了toString。
1.Array的toString是:每个元素转换为字符串,两个元素之间用英文逗号隔开连接。
空数组没有元素转为空字符串 ""
2.Boolean的toString是:如果布尔值是true,则返回"true"。否则返回"false""。
3.Date的toString是:返回日期的文本。
3. toString方法的三个作用
1.返回一个【表示对象】的【字符串】 "[object 类型]"
2.检测对象的类型
Object.prototype.toString.call(arr)==="[object Array]"
3.返回该数字对应进制的字符串。
console.log(10.toString(2)) //10专为为2进制'1010'
在js中包含2进制,8进制,10进制,16进制。
4. js几种模块的规范
有四种成熟的模块规范。
1.CommonJS。通过require来引入,通过module.exports输出接口。
他是服务端的解决方案,以同步的方式来引入模块。
如果是在浏览器端,模块的加载使用网络,同步的方式就不太好。
2. AMD方案他采用的是异步加载模块。
模块的加载不影响后面语句的执行。
所有依赖这个模块的语句都在载一个回调函数中,
等待模块加载完成后再执行回调函数
require.js 实现了 AMD 规范。
3. CMD方案:异步加载的方式来加载模块
sea.js 实现了 CMD 规范
第4种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块。
5. undefined 和 undeclared 的区别
undefined:在作用域中申明变量但是没有赋值,
var a; 当我们访问a就是undefined
undeclared:在作用域中没有申明过的变量;如 b;
当我们访问b就是 undeclared;
控制台报错:b is not defined
其实“ undefined” 和“ is not defined ”是两码事。
并且typeof对undefined 和 undeclared 变量返回的都是undefined。
6. javascript 创建对象的几种方式?
1.字面量的方式去创建对象。 var person1={ age:12,name:'zs'}
2.使用new字符创建对象
var person2=new Object();
person2.name='柯南'
【虽然上面这2种都可以去创建对象
但是我们创建出来的对象我们无法判断是什么类型。
】
3.自定义构造函数创建对象
function Person(name,age,){
this.name=name;
this.age=age;
this.say=function(){
console.log("我叫",name)
}
}
let per1=new Person('柯南',19,'男');
//这样就可以判断属于什么类型
console.log(per1 instanceof Person);//true
4.工厂模式创建对象,【工厂模式new一个obj,然后设置这个obj的相关属性,最后再然后】
function createObj(age) {
let obj=new Object();
obj.age=age;
obj.sayHi=function(){
console.log(obj.name)
}
return obj;
}
let per=createObj('司藤',200)
[kɑːmən ] CommonJS 普遍的
[e k s po s] exports 输出
[舍 | si] sea 海洋
undeclared 未承认的,未申明的 [ʌn dɪ k li ə d]
7.当我们new一个对象的时候做了四件事情
1.开辟空间存储当前对象
2.把this设置为当前对象
3.设置属性和方法
3.把this对象返回
8.防抖
当持续触发事件时,【一定时间段内】该事件没有被触发。
事件处理函数才会被执行一次。
如果设定的时间到来之前,又触发了事件,就重新开始延时。
9.函数节流
在规定的单位时间内,只能够触发一次。
也就是在规定的时间内某事件被多次触发,那么只能够生效一次.
10.什么是回调函数 ? 缺点
回调函数是一段可执行的代码,
它作为一个参数传递给其他的代码.
其作用是在需要的时候方便调用这段(回调函数)代码。
回调函数有一个致命的弱点,
在多个事件存在依赖性,
就是容易写出回调地狱(Callback hell)。
11.什么是对象解构?
对象析构是从对象或数组中获取值的一种新的、更简洁的方法。
当我们的对象有很多的属性和方法,通过对象点属性的时候会很麻烦
如果使用对象解构,就会变得非常的简单。
假设有如下的对象
const employee = {
firstName: "Marko",
lastName: "Polo",
};
我们要获取值:
var firstName = employee.firstName;
当我们的对象有很多的属性和方法,用这种方法提取属性会很麻烦?
使用解构就非常的简单
{ firstName, lastName, position, yearHired } = employee;
我们还可以为属性取别名:
let { firstName: fName, lastName: lName, position, yearHired } = employee;
12.(ES6)有哪些新特性?
let const
模板字符串
解构
箭头函数
类
Promise
模块
Symbol
代理(proxy)
函数默认参数
13.函数声明与函数表达式的区别?
在Javscript中,解析器在执行代码的时候。
解析器会率先读取函数声明,
并【使其】在执行任何代码之前可用(可以访问)。
但是在执行函数表达式的时候,
则必须等到解析器执行到它所在的代码时,才会真正被解析执行。
14. 当一个DOM节点被点击时候,我们希望能够执行一个函数,应该怎么做?
<button onclick="xxx()"></button>
box.onlick= function(){}
box.addEventListener("click",function(){},false);
15 dom选择器优先级是什么,以及权重值计算(一道老问题了)
1.行内样式 1000
2.id 0100
3.类选择器、伪类选择器、属性选择器[type=“text”] 0010
4.标签选择器、伪元素选择器(::first-line) 0001
16. dom节点的根节点是不是body
不是,
dom节点的根节点是html
(包含head和body,head中分为meta、title等。body又分为一组)
17. 怎么判断两个对象相等
JSON.stringify(obj)==JSON.stringify(obj);//true
18.重绘和重排
重绘:当元素的一部分属性发生改变,
如背景、颜色等不会引起布局变化,
只需要浏览器根据元素的新属性重新绘制,
使元素呈现新的外观叫做重绘。
重排(回流):当render树中的一部分或者全部,
因为大小边距等问题,
发生改变而需要DOM树重新计算的过程
所以简单的来说就是。不会引起布局的变化,叫做重绘。
会引起布局的变化,叫做重排(回流)
所以在我们平时写css的时候,还是要按照html中类的顺序来写。
否者可能会造成重排,重排是需要消耗浏览器性能的哈。
19.引起重排的地方
1.添加、删除可见的dom
2.元素的位置改变
3.元素的尺寸改变(外边距、内边距、边框厚度、宽高)
4.页面渲染初始化
5.浏览器窗口大小改变
7.改变文字大小
10.操作class属性
11.内容的改变,(用户在输入框中写入内容也会)
20 请尽可能详尽的解释AJAX的工作原理。
1.创建ajax对象(XMLHttpRequest/ActiveXObject(Microsoft.XMLHttp))
2.判断数据传输方式(GET/POST)
3.打开链接 open()
4.发送 send()
5.当ajax对象完成第四步(onreadystatechange)数据接收完成,判断http响应状态(status)200-300之间或者304(缓存)执行回调函数
21、ios滑动卡顿
-webkit-overflow-scrolling:touch 可能会在IOS系统低的情况出现滚动条;尝试溢出解决
、forEach和map的区别
相同点:
1.都是循环遍历数组中的每一项
2.forEach和map函数都支持3个参数,参数分别是item(当前每一项)、index(索引值)、arr(原数组)
3.匿名函数中的this都是指向window
4.只能遍历数组
都有兼容问题
不同点:
map速度比foreach快
map会返回一个新数组,不对原数组产生影响.foreach不会产生新数组,
map因为返回数组所以可以链式操作,foreach不能
get和post有什么区别?get可以通过body传递数据吗
把数据放到 body 里面,必须用 POST 方式取,这是 HTTP 协议限制的。
有趣的面试题
var User = {
count: 1,
getCount: function() {
return this.count;
}
};
console.log(User.getCount()); // what? 1
var func = User.getCount;
console.log(func()); // what? undefined
13.构造函数和实例对象和原型之间的关系 [想你画的那个图]
1.构造函数可以实例化对象
2.构造函数中有一个属性叫做prototype,是构造函数的原型对象。
{构造函数有两个箭头指向出去了的.}
3.构造函数的原型对象{prototype}中有一个constructor构造器,
这个构造器是指向[自己所在的原型对象]所在的[构造函数]。
4.构造函数的原型对象prototype中的方法可以被实例对象直接访问的。
5.实例对象的原型的对象(__proto__)指向的是该构造函数的原型对象。
1. 函数柯里化的实现,去了解一下这个东西是个什么玩意?
2.函数的深拷贝和浅拷贝是非常的复杂的。
3.什么是FOUC?你如何来避免FOUC?
JavaScript的许多内置对象都重写了toString。
1.Array的toString是:每个元素转换为字符串,两个元素之间用英文逗号隔开连接。
空数组没有元素转为空字符串 ""
2.Boolean的toString是:如果布尔值是true,则返回"true"。否则返回"false""。
3.Date的toString是:返回日期的文本。
1.返回一个【表示对象】的【字符串】 "[object 类型]"
2.检测对象的类型
Object.prototype.toString.call(arr)==="[object Array]"
3.返回该数字对应进制的字符串。
console.log(10.toString(2)) //10专为为2进制'1010'
在js中包含2进制,8进制,10进制,16进制。
有四种成熟的模块规范。
1.CommonJS。通过require来引入,通过module.exports输出接口。
他是服务端的解决方案,以同步的方式来引入模块。
如果是在浏览器端,模块的加载使用网络,同步的方式就不太好。
2. AMD方案他采用的是异步加载模块。
模块的加载不影响后面语句的执行。
所有依赖这个模块的语句都在载一个回调函数中,
等待模块加载完成后再执行回调函数
require.js 实现了 AMD 规范。
3. CMD方案:异步加载的方式来加载模块
sea.js 实现了 CMD 规范
第4种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块。
undefined:在作用域中申明变量但是没有赋值,
var a; 当我们访问a就是undefined
undeclared:在作用域中没有申明过的变量;如 b;
当我们访问b就是 undeclared;
控制台报错:b is not defined
其实“ undefined” 和“ is not defined ”是两码事。
并且typeof对undefined 和 undeclared 变量返回的都是undefined。
1.字面量的方式去创建对象。 var person1={ age:12,name:'zs'}
2.使用new字符创建对象
var person2=new Object();
person2.name='柯南'
【虽然上面这2种都可以去创建对象
但是我们创建出来的对象我们无法判断是什么类型。
】
3.自定义构造函数创建对象
function Person(name,age,){
this.name=name;
this.age=age;
this.say=function(){
console.log("我叫",name)
}
}
let per1=new Person('柯南',19,'男');
//这样就可以判断属于什么类型
console.log(per1 instanceof Person);//true
4.工厂模式创建对象,【工厂模式new一个obj,然后设置这个obj的相关属性,最后再然后】
function createObj(age) {
let obj=new Object();
obj.age=age;
obj.sayHi=function(){
console.log(obj.name)
}
return obj;
}
let per=createObj('司藤',200)
[kɑːmən ] CommonJS 普遍的
[e k s po s] exports 输出
[舍 | si] sea 海洋
undeclared 未承认的,未申明的 [ʌn dɪ k li ə d]
1.开辟空间存储当前对象
2.把this设置为当前对象
3.设置属性和方法
3.把this对象返回
当持续触发事件时,【一定时间段内】该事件没有被触发。
事件处理函数才会被执行一次。
如果设定的时间到来之前,又触发了事件,就重新开始延时。
在规定的单位时间内,只能够触发一次。
也就是在规定的时间内某事件被多次触发,那么只能够生效一次.
回调函数是一段可执行的代码,
它作为一个参数传递给其他的代码.
其作用是在需要的时候方便调用这段(回调函数)代码。
回调函数有一个致命的弱点,
在多个事件存在依赖性,
就是容易写出回调地狱(Callback hell)。
对象析构是从对象或数组中获取值的一种新的、更简洁的方法。
当我们的对象有很多的属性和方法,通过对象点属性的时候会很麻烦
如果使用对象解构,就会变得非常的简单。
假设有如下的对象
const employee = {
firstName: "Marko",
lastName: "Polo",
};
我们要获取值:
var firstName = employee.firstName;
当我们的对象有很多的属性和方法,用这种方法提取属性会很麻烦?
使用解构就非常的简单
{ firstName, lastName, position, yearHired } = employee;
我们还可以为属性取别名:
let { firstName: fName, lastName: lName, position, yearHired } = employee;
let const
模板字符串
解构
箭头函数
类
Promise
模块
Symbol
代理(proxy)
函数默认参数
在Javscript中,解析器在执行代码的时候。
解析器会率先读取函数声明,
并【使其】在执行任何代码之前可用(可以访问)。
但是在执行函数表达式的时候,
则必须等到解析器执行到它所在的代码时,才会真正被解析执行。
<button onclick="xxx()"></button>
box.onlick= function(){}
box.addEventListener("click",function(){},false);
1.行内样式 1000
2.id 0100
3.类选择器、伪类选择器、属性选择器[type=“text”] 0010
4.标签选择器、伪元素选择器(::first-line) 0001
不是,
dom节点的根节点是html
(包含head和body,head中分为meta、title等。body又分为一组)
JSON.stringify(obj)==JSON.stringify(obj);//true
重绘:当元素的一部分属性发生改变,
如背景、颜色等不会引起布局变化,
只需要浏览器根据元素的新属性重新绘制,
使元素呈现新的外观叫做重绘。
重排(回流):当render树中的一部分或者全部,
因为大小边距等问题,
发生改变而需要DOM树重新计算的过程
所以简单的来说就是。不会引起布局的变化,叫做重绘。
会引起布局的变化,叫做重排(回流)
所以在我们平时写css的时候,还是要按照html中类的顺序来写。
否者可能会造成重排,重排是需要消耗浏览器性能的哈。
1.添加、删除可见的dom
2.元素的位置改变
3.元素的尺寸改变(外边距、内边距、边框厚度、宽高)
4.页面渲染初始化
5.浏览器窗口大小改变
7.改变文字大小
10.操作class属性
11.内容的改变,(用户在输入框中写入内容也会)
1.创建ajax对象(XMLHttpRequest/ActiveXObject(Microsoft.XMLHttp))
2.判断数据传输方式(GET/POST)
3.打开链接 open()
4.发送 send()
5.当ajax对象完成第四步(onreadystatechange)数据接收完成,判断http响应状态(status)200-300之间或者304(缓存)执行回调函数
-webkit-overflow-scrolling:touch 可能会在IOS系统低的情况出现滚动条;尝试溢出解决
相同点:
1.都是循环遍历数组中的每一项
2.forEach和map函数都支持3个参数,参数分别是item(当前每一项)、index(索引值)、arr(原数组)
3.匿名函数中的this都是指向window
4.只能遍历数组
都有兼容问题
不同点:
map速度比foreach快
map会返回一个新数组,不对原数组产生影响.foreach不会产生新数组,
map因为返回数组所以可以链式操作,foreach不能
把数据放到 body 里面,必须用 POST 方式取,这是 HTTP 协议限制的。
var User = {
count: 1,
getCount: function() {
return this.count;
}
};
console.log(User.getCount()); // what? 1
var func = User.getCount;
console.log(func()); // what? undefined
1.构造函数可以实例化对象
2.构造函数中有一个属性叫做prototype,是构造函数的原型对象。
{构造函数有两个箭头指向出去了的.}
3.构造函数的原型对象{prototype}中有一个constructor构造器,
这个构造器是指向[自己所在的原型对象]所在的[构造函数]。
4.构造函数的原型对象prototype中的方法可以被实例对象直接访问的。
5.实例对象的原型的对象(__proto__)指向的是该构造函数的原型对象。
https://juejin.cn/post/6844904200917221389#heading-63
什么是Proxy?
js 的几种模块规范?
有四种成熟模块加载方案:
第1种是 CommonJS 方案,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。
这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的.
因为在服务端文件都存储在本地磁盘,所以读取非常快.所以以同步的方式加载没有问题。
但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块.
模块的加载不影响后面语句的执行.
所有依赖这个模块的语句都定义在一个回调函数里.
等到加载完成后再执行回调函数。
require.js 实现了 AMD 规范。
第三种是 CMD 方案,这种方案和 AMD 方案都是为了解决异步模块加载的问题,
sea.js 实现了 CMD 规范。
它和require.js的区别在于:
模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
第四种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块。
面试手写(原生):
//1:创建Ajax对象
var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');// 兼容IE6及以下版本
//2:配置 Ajax请求地址
xhr.open('get','index.xml',true);
//3:发送请求
xhr.send(null); // 严谨写法
//4:监听请求,接受响应
xhr.onreadysatechange=function(){
if(xhr.readySate==4&&xhr.status==200 || xhr.status==304 ){
console.log(xhr.responsetXML)
}
}
微任务有哪些?
1.Promise 2.await和async
这样记忆: es6(Promise) es7(await和async)
宏任务有哪些?
1.setTimeout 2.setInterval
3.DOM事件 4.AJAX请求
微任务与宏任务的区别?
微任务的执行时机比宏任务早。
但是并不不意味着=>微任务的结果先返回数据
严格的来说:ps:微任务》DOM渲染》宏任务 .
js的基本数据类型有哪些?
JavaScript一共有6种数据类型
其中有7种基本数据类型:String,Boolean、Number,Undefined,Null,Symbol
数据类型的转化
转换为布尔值--调用Boolean()方法
转换为数字-调用Number()、parseInt()、parseFloat()方法
转换为字符串调用.toString()或者 加上一个空的字符串( +'' )
ps:null和underfined没有.toString方法
有哪些内置对象?
Function、Arguments、Math、Date、RegExp(正则表达式)、Error
Ajax 是什么?
我对 ajax 的理解是,它是一种异步通信的方法.
通过js脚本向服务器发起 http 通信,
然后根据服务器返回的数据,更新网页的相应部分.
而不用刷新整个页面的一种方法。
如何创建一个 Ajax?
1.创建Ajax对象 2.配置 Ajax请求地址 3.发送请求 4:监听请求,接受响应
//1:创建Ajax对象 兼容IE6及以下版本
var xhr = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP');
//2:配置 Ajax请求地址
xhr.open('get','index.xml',true); //
//3:发送请求
xhr.send(null); // 严谨写法
//4:监听请求,接受响应
xhr.onreadysatechange=function(){
if(xhr.readySate ==4 && xhr.status==200 || xhr.status==304 )
console.log(xhr.responsetXML)
}
谈谈你对模块化开发的理解?(高级)
我的理解:一个模块是实现一个特定功能的一组方法,
在最开始的时候,js 只实现一些简单的功能,所以并没有模块的概念 .
但随着程序越来越复杂,代码的模块化开发变得越来越重要。
由于函数具有独立作用域的特点,最原始的写法是使用函数来作为模块,
但是这种方式容易造成全局变量的污染,并且模块间没有联系。
后来提出了对象写法,通过将函数作为一个对象的方法来实现,
这样解决了直接使用函数作为模块的一些缺点。
但是这种办法会暴露所有的所有的模块成员,外部代码可以修改内部属性的值。
所以现在最常用的是立即执行函数的写法,
通过利用闭包来实现模块私有作用域的建立,同时不会对全局作用域造成污染。
AMD 和 CMD 规范的区别?
它们之间的主要区别有两个方面:
1.AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块。
而 CMD 推崇就近依赖,只有在用到某个模块的时候再去 require
2.对依赖模块的执行时机处理不同。
AMD在依赖模块加载完成后就直接执行依赖模块。
CMD 在依赖模块加载完成后并不执行,只是下载而已,
等到所有的依赖模块都加载好后,进入回调函数函数,
遇到 require 语句 的时候才执行对应的模块,
(这样模块的执行顺序就和我们书写的顺序保持一致了)
requireJS的核心原理是什么?(高级)
require.js 的核心原理是通过动态创建 script 脚本,来异步引入模块,
然后对每个脚本的 load 事件进行监听,
如果每个脚本都加载完成了,再调用回调函数。
ps:require.js 实现了 AMD 规范。
ES6 模块与 CommonJS 模块的差异(高级)
1=>CommonJS 模块输出的是一个值的拷贝, CommonJS 模块输出的是值
ES6 模块输出的是值的引用。
2=> CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
CommonJS 模块就是对象,即在输入时是先加载整个模块,生成一个对象,
然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
arguments 的对象是什么?
arguments对象是函数中传递的参数值的集合。
它是一个类似数组的对象,因为它有一个length属性.
使用数组索引表示法arguments[1]可以访问单个值。
但它没有数组中的内置方法,如:forEach、reduce、filter和map。
通过Array.prototype.slice将arguments对象转换成一个数组。
注意:箭头函数中没有arguments对象。
说说浏览器的内核?
浏览器名称 内核
谷歌浏览器(chrome) blink
欧朋浏览器(Opera) blink
火狐浏览器(firefox) Gecko
苹果浏览器(safari) webkit
ps:Blink其实是Webkit的分支
你对浏览器内核的理解?
浏览器内核也就是我们说的渲染引擎。
负责对网页语法进行解释。
如将HTML,JavaScript渲染(显示)为网页。
渲染引擎决定了浏览器如何显示网页的内容以及页面的格式信息。
不同的浏览器内核对网页编写语法的解释不同
简单介绍一下 V8 引擎的垃圾回收机制 高级
v8 引擎将内存分为了新生代和老生代。
新创建的对象或者只经历过一次的垃圾回收的对象被称为新生代。
经历过多次垃圾回收的对象被称为老生代。
新生的对象容易早死,老生代活得更久
新生代被分为 From 和 To 两个空间。
To 一般是闲置的,当 From 空间满了的时候会执行 Scavenge 算法进行垃圾回收。
这个算法分为三步:
1.首先检查 From 空间的存活对象,
如果判断这个存活对象是否满足晋升到老生代的条件,
满足条件则晋升到老生代,如果不满足条件则移动 To 空间。
2.如果对象不存活,则释放对象的空间。
3.最后将 From 空间和 To 空间角色进行交换
V8 新生代对象晋升到老生代有两个条件:
1.第一个是判断对象否已经经过一次 Scavenge 回收。
若经历过,则将该对象从 From 空间复制到老生代中;
若没有经历,则复制到 To 空间。
2.第二个是 To 空间的内存使用占比是否超过限制。
当对象从 From 空间复制到 To 空间时,若 To 空间使用超过 25%,
则对象直接晋升到老生代中。
设置 25% 的原因主要是因为算法结束后,
两个空间结束后会交换位置,如果 To 空间的内存太小,会影响后续的内存分配。
ps:老生代采用了标记清除法和标记压缩法。
参考的地址:https://juejin.cn/post/6844904200917221389#heading-43
ECMAScript 是什么?
ECMAScript 是编写脚本语言的标准,
这意味着JavaScript遵循ECMAScript的标准规范。
ECMAScript 和 Javascript,本质上都跟一门语言有关,一个是语言的约束条件,一个是语言本身的名字。
只不过发明JavaScript的那个人(网景公司),把东西交给了ECMA,
让ECMA规定一下JavaScript的标准,所以这样一个神奇的东西诞生了,这个东西的名称就叫做ECMAScript。
javaScript = ECMAScript + DOM + BOM(自认为是一种广义的JavaScript)
ECMAScript说什么JavaScript就得做什么!
JavaScript(狭义的JavaScript)做什么都要问问ECMAScript我能不能这样干!如果不能我就错了!能我就是对的!
——突然感觉JavaScript好没有尊严,为啥要搞个人出来约束自己,
那个人被创造出来也好委屈,自己被创造出来完全是因为要约束JavaScript。
JS的9种前端常见的设计模式
1. 外观模式:
比如JQuery就把复杂的原生DOM操作进行了抽象和封装,并消除了浏览器之间的兼容问题.
2. 代理模式:是为一个对象提供一个代用品或占位符,以便控制对它的访问
3. 工厂模式
4. 单例模式
5. 策略模式
6. 迭代器模式
7. 观察者模式
8. 中介者模式
9. 访问者模式
参考的链接:
https://juejin.cn/post/6844904200917221389
https://blog.csdn.net/weixin_45151960/article/details/104832916