JS原生基础面试题 以及内置源码重写 -->后期还会更新

1,190 阅读15分钟

前言

文章涉及的内容可能不全面,但量很多,需要慢慢看。我花了很长的时间整理,用心分享心得,希望对大家有所帮助。但是难免会有打字的错误或理解的错误点,希望发现的可以邮箱告诉我ghui_master@163.com,我会及时的进行修改,只希望对你有所帮助,谢谢。

JS中的数据类型都有哪些?它们之间有什么区别?该如何检测?

基本数据类型:

number、

string、

boolean、

null、

undefined 引用数据类型: object(普通对象、数组对象、正则对象、日期对象、Math、实例、 prototype等)

function(普通函数和类) 特殊数据类型:Symbol

基本数据类型按值操作,引用数据类型按照堆内存的引用地址来操作

数据类型检测四种方式: typeof

instanceof

constructor

Object.prototype.toString.call()

  1. 常用浏览器的内核都有哪些? 3. 数组中常用的迭代方法有哪些?都是什么意思?(至少四种)

    webkit、

    Gecko、

    Trident、

    Presto等

  2. 说一下你对闭包的理解,以及工作中什么地方你用到了闭包?

    函数执行会形成一个私有的作用域(私有栈内存),这就是闭包,在真实项目 中主要应用闭包的两大作用 保护

    保存 之前研究过像JQUERY等类库的源码,为了防止全局变量污染,基本上所有代码 都是放到闭包中保护起来的 项目中遇到循环事件绑定,也会基于闭包保存的作用,去存储对应的索引 之前研究过JS高阶编程技巧:柯理化函数编程思想,这个就是闭包的应用,重 写过debounce、throttle函数节流和防抖的方法 ...... 因为闭包会产生不释放的栈内存,所以尽可能在项目中少使用

    什么是面向对象? 谈谈你的理解

    面向对象是一种编程思想,js本身就是基于面向对象构建出来的(例如:js中有很多内置类,像Promise就是ES6中新增的一个内置类,我们可以基于new Promise来创建一个实例,来管理异步编程,我们在项目中Promise也经常用,自己也研究过他的源码。。。)我们平时用的VUE/REACT/JQUERY也是基于面向对象构建出来的,他们都是类,平时开发的时候都是创建他们的额实例来操作的;当然我自己在真实项目中,也封装过一些组件插件,(例如:DIALOG、拖拽、、、)也是基于面向对象开发的,这样可以创造不同的实例,来管理私有的属性和公有的方法,很方便。。。。

    JS中的面向对对象,和其他编程语言还略有不同,JS中类和实例都是基于原型和原型链机制来处理的; 而且js中关于类的重载、重写、继承也和其他语言不太一样

  3. window.onload VS $(document).ready()

    // 这个题我知道,我之前看过部分JQ源码

    // 1.$(document).ready() 采用的是DOM2事件绑定,监听的是DOMContentLoaded这个事件,所以只要DOM结构加载完成就会被触发执行,而且同一个页面中可以使用多次(绑定不同的方法,因为基于DOM2事件池绑定机制完成的) // 2.window.onload必须等待所有资源都加载完成才会被触发执行,采用DOM0事件绑定,同一个页面只能绑定一次(一个方法),想绑定多个也需要改为window.addEventListener('load', function () {})DOM2绑定方式

  4. 阐述一下let/var/const三者之间的区别?

    let 和 var 的区别

    1. 不存在变量提升
    2. 不允许重复声明
    3. 在全局作用域下设置变量不会给window设置属性
    4. 存在块级作用域
    5. 解决了一些暂时性死区问题

    let 和 const 的区别 const 创建的是常量,存储的值不能被修改(准确说是不能修改变量的指 向)

  5. 阐述一下call/apply/bind三者之间的区别,以及应用场景?

    call 和 apply 的区别 给方法传参的时候,call是按顺序,一个个传递;apply是把传递的参数放 到一个数组中传递;

    在传递参数较多的情况下, call的性能要高于apply; call 和 bind 的区别 call在改变函数this指向的时候,会把函数立即执行,而bind不会把函数立 即执行,只是预先处理了this和实参信息;

    真实项目中,我们需要改变this指向的时候,会应用这三个方法,例如: 给元素进行事件绑定,我们需要把事件触发,所执行的函数中this和参数进 行预先处理,此时可以使用bind进行处理;

    我们可以基于call方法,让类数组借用数组原型上的方法,例如:把类数组 转换为数组

    可以基于apply传参是一个数组,借用Math.max获取数组中的最大值 ......

    数组中常用的迭代方法有哪些?(至少四种)

    forEach、

    map、

    find、

    some、

    filter、

    reduce、

    every、

    sort等

    有A和B两个开发者,他们同时开发一款产品(最后的代码需要合并),为了防止相互之前产生变量污染,他们决定采用高级单例模式来进行模块化开发,请编写相应的代码示例!

    // 开发者A
    let AModule = (function () {
    	let n = 10;
    	let query = function () {
    		//...
    	};
    	let fn = function () {
    		//...
    		//调取开发者B编写的QUERY方法
    		BModule.query();
    	};
    
    	return {
    		query: query,
    		init: function () {
    			query();
    			fn();
    		}
    	}
    })();
    
    // 开发者B
    let BModule = (function () {
    	let n = 20;
    	let query = function () {
    		//...
    	};
    	let sum = function () {
    		//...
    		//调取开发者A编写的QUERY方法
    		AModule.query();
    	};
    
    	return {
    		query: query,
    		init: function () {
    			query();
    			sum();
    		}
    	}
    })();
    
    AModule.init();
    BModule.init();
     
    

    ES6中的新语法规范

    • let / const
    • class 创建类
    • import / export :ES6 Module 模块的导入导出规范(JS中的模块化规范 AMD -> CMD -> CommonJS -> ES6 Module)
    • Arrow Function 箭头函数
    • 模板字符串
    • 解构赋值
    • “...” 拓展、展开、剩余运算符
    • Promise / async / await
    • for of循环
    • Set / Map
    • Array / Object ... 提供的新方法
    • ......

    var r2 = typeof typeof typeof sum; // "string"

    执行顺序:先执行最后一个 typeof sum -> "function" -> typeof typeof "function" -> typeof "string" -> "string"

    只要是两个及以上的typeof 最后返回结果就是 "string"

    TCP传输的详细过程是怎样的?

    进行三次握手,建立TCP连接。
  • 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;

  • 第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;

  • 第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。

  • 完成了三次握手,客户端和服务器端就可以开始传送数据。

  • ACK:此标志表示应答域有效,就是说前面所说的TCP应答号将会包含在TCP数据包中;有两个取值:0和1,为1的时候表示应答域有效,反之为0。

  • TCP协议规定,只有ACK=1时有效,也规定连接建立后所有发送的报文的ACK必须为1。

  • SYN(SYNchronization) : 在连接建立时用来同步序号。当SYN=1而ACK=0时,表明这是一个连接请求报文。对方若同意建立连接,则应在响应报文中使SYN=1和ACK=1. 因此, SYN置1就表示这是一个连接请求或连接接受报文。 FIN (finis)即完,终结的意思, 用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已经发送完毕,并要求释放连接。

  • 发送HTTP请求,服务器处理请求,返回响应结果 TCP连接建立后,浏览器就可以利用HTTP/HTTPS协议向服务器发送请求了。服务器接受到请求,就解析请求头,如果头部有缓存相关信息如if-none-match与if-modified-since,则验证缓存是否有效,若有效则返回状态码为304,若无效则重新返回资源,状态码为200. 关闭TCP连接

  • 第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;

  • 第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;

  • 第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;

  • 第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。

了解跨域吗,一般什么情况下会导致跨域

小提示: 如果平常自身有使用场景可结合使用场景进行讲解,比如我在这里使用过的场景是CORS和Nginx反向代理。

跨域行为

  • 同源策略限制、安全性考虑
  • 协议、IP和端口不一致都是跨域行为

JSONP

小提示:如果你提到JSONP,面试官肯定会问你整个详细的实现过程,所以一定要搞懂JSONP的实现原理,如果不是很理解可以自己起一个Express服务实践一下。

Web前端事先定义一个用于获取跨域响应数据的回调函数,并通过没有同源策略限制的script标签发起一个请求(将回调函数的名称放到这个请求的query参数里),然后服务端返回这个回调函数的执行,并将需要响应的数据放到回调函数的参数里,前端的script标签请求到这个执行的回调函数后会立马执行,于是就拿到了执行的响应数据。

缺点: JSONP只能发起GET请求

如何实现一个JSONP

这里给出几个链接:

segmentfault.com/a/119000001… zhangguixu.github.io/2016/12/02/… www.cnblogs.com/iovec/p/531…

JSONP安全性问题

CSRF攻击

前端构造一个恶意页面,请求JSONP接口,收集服务端的敏感信息。如果JSONP接口还涉及一些敏感操作或信息(比如登录、删除等操作),那就更不安全了。

解决方法:验证JSONP的调用来源(Referer),服务端判断Referer是否是白名单,或者部署随机Token来防御。

XSS漏洞

不严谨的 content-type导致的 XSS 漏洞,想象一下 JSONP 就是你请求 http://youdomain.com?callback=douniwan, 然后返回 douniwan({ data }),那假如请求 http://youdomain.com?callback=<script>alert(1)</script> 不就返回 <script>alert(1)</script>({ data })了吗,如果没有严格定义好 Content-Type( Content-Type: application/json ),再加上没有过滤 callback 参数,直接当 html 解析了,就是一个赤裸裸的 XSS 了。

解决方法:严格定义 Content-Type: application/json,然后严格过滤 callback 后的参数并且限制长度(进行字符转义,例如<换成&lt,>换成&gt)等,这样返回的脚本内容会变成文本格式,脚本将不会执行。

服务器被黑,返回一串恶意执行的代码

可以将执行的代码转发到服务端进行校验JSONP内容校验,再返回校验结果。

CORS(跨域资款共享)

小提示:如果你回答跨域解决方案CORS,那么面试官一定会问你实现CORS的响应头信息Access-Control-Allow-Origin。

什么是CORS

CORS(跨域资源共享 Cross-origin resource sharing)允许浏览器向跨域服务器发出XMLHttpRequest请求,从而克服跨域问题,它需要浏览器和服务器的同时支持。

  • 浏览器端会自动向请求头添加origin字段,表明当前请求来源。
  • 服务器端需要设置响应头的Access-Control-Allow-Methods,Access-Control-Allow-Headers,Access-Control-Allow-Origin等字段,指定允许的方法,头部,源等信息。
  • 请求分为简单请求和非简单请求,非简单请求会先进行一次OPTION方法进行预检,看是否允许当前跨域请求。

简单请求

请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

HTTP的请求头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

后端的响应头信息:

  • Access-Control-Allow-Origin:该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
  • Access-Control-Allow-Credentials:该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。
  • Access-Control-Expose-Headers:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
  • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。

JSONP和CORS的对比

  • JSONP只支持GET请求,CORS支持所有类型的HTTP请求
  • JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据

其他跨域解决方案

  • Nginx反向代理

  • postMessage

  • document.domain

    基于原生实现内置类方法重写

    重写内置 new

    function Dog(name) {
    this.name = name;
    }
    Dog.prototype={
    bark(){
    console.log('wangwang');
    },
    sayName(){
    console.log('my name is ' + this.name);
    }
    };
    function _new() {
    //=>完成你的代码
    }
    let sanmao = _new(Dog, '三毛');
    sanmao.bark(); //=>"wangwang"
    sanmao.sayName(); //=>"my name is 三毛"
    console.log(sanmao instanceof Dog); //=>true
    ================================================
     //——> 重写 _new
    function _new(Fn, ...arg) {
    let obj = {};
    obj.__proto__ = Fn.prototype;
    Fn.call(obj, ...arg);
    return obj;
    }
    

    实现一个$attr(name,value)遍历

    /* 实现一个$attr(name,value)遍历
    * 属性为name
    * 值为value的元素集合
    * 例如下面示例:
    */
    let ary = $attr('class','box'); //=>获取页面中所有class为box的元素
    =====================================
         
         
    function $attr(property, value) {
    let elements = document.getElementsByTagName('*'),
    arr = [];
    elements = Array.from(elements);
    elements.forEach(item => {
    let itemValue = item.getAttribute(property);
    if (property==='class') {
    new RegExp("\\b" + value +
    "\\b").test(itemValue)?arr.push(item):null;
    return;
    }
    if (itemValue === value) {
    arr.push(item);
    }
    });
    return arr;
    }
    console.log($attr('class', 'box'));
    

    bind重写

    ~function(){
    //=>bind方法在IE6~8中不兼容,接下来我们自己基于原生JS实现这个方法
    function bind(){
    };
    Function.prototype.bind=bind;
    }();
    var obj = {name:'zhufeng'};
    function func(){
    console.log(this,arguments);
    //=>当点击BODY的时候,执行func方法,输出:obj
    [100,200,MouseEvent事件对象]
    }
    document.body.onclick = func.bind(obj,100,200);
    
    =============================================
     ~function(){
    //=>bind方法在IE6~8中不兼容,接下来我们自己基于原生JS实现这个方法
    function bind(context){
    context=context||window;
    var _this = this,
    outerArg=[].slice.call(arguments,1);
    return function anonymous() {
    var innerArg=[].slice.call(arguments,0);
    _this.apply(context, outerArg.concat(innerArg));
    }
    };
    Function.prototype.bind=bind;
    }();    
    

    class重构

    function Modal(x,y){
    this.x=x;
    this.y=y;
    }
    Modal.prototype.z=10;
    Modal.prototype.getX=function(){
    console.log(this.x);
    }
    Modal.prototype.getY=function(){
    console.log(this.y);
    }
    Modal.n=200;
    Modal.setNumber=function(n){
    this.n=n;
    };
    let m = new Model(10,20);
    ===============================================
         
    class Modal{
    constructor(x,y){
    this.x=x;
    this.y=y;
    }
    getX(){
    console.log(this.x);
    }
    getY(){
    console.log(this.y);
    }
    static setNumber(n){
    this.n=n;
    }
    }
    Modal.n=200;
    Modal.prototype.z=10;     
    

    call重写

    ~function(){
        function changeThis(context){
           context=context||window;
            //let arg=[].slice.call(arguments,1);
            let arg=[],
                _this=this,
                result=null;
            for(let i=1;i<arguments.length;i++){
                arg.push(arguments[i]);
            }
            context.$fn=_this;
            result=context.$fn(...arg);
            delete context.$fn;
            return result;
       };
        Function.prototype.changeThis=changeThis;
    }();
    
    let obj = {name:'Alibaba',$fn:100};
    function fn(x,y){
        this.total=x+y;
        return this;
    }
    let res = fn.changeThis(obj,100,200);
    //res => {name:'Alibaba',total:300}
    

    apply重写

    ~function(){
        /*生成随机函数名:时间戳的方式*/
        function queryRandomName(){
            let time=new Date().getTime();
            return '$zhufeng'+time;
        }
        /*模拟CALL方法改变函数中的THIS*/
        function changeThis(context=window,arg=[]){
            let _this=this,
                result=null,
                ran=queryRandomName();
            context[ran]=_this;
            result=context[ran](...arg);
            delete context[ran];
            return result;
       };
        Function.prototype.changeThis=changeThis;
    }();
    let res = fn.changeThis(obj,[100,200]);
    

    怎么让一个 div 水平垂直居中?(不少于三种解决方案)

/* 已知宽高 */
.box {
position: absolute;
top: 50%;
left: 50%;
margin-top: -50px;
margin-left: -50px;
width: 100px;
height: 100px;
}
========================
/* 未知宽高 */
.box {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
==========================
.box {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
margin: auto;
}
===============================
.container {
display: flex;
justify-content: center;
align-items: center;
}
.container .box {}

编写一个方法“flatten”,将数组扁平化 (至少两种办法)

/*
* 1.编写一个方法“flatten”,将数组扁平化 (至少两种办法)
* 2.然后实现“unique”数组去重方法,把数组进行去重 (至少两种办法)
*/
let arr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9,[11, 12, [12,
13, [14]]]], 10];
ary.flatten().unique().sort((a,b)=>a-b); //=>[1, 2, 3, 4, 5,
6, 7, 8, 9....]
============================================
~function(){
function flatten(){
/*第一种*/
return JSON.stringify(this).replace(/(\
[|\])/g,'').split(',').map(item=>{
return Number(item);
});
/*第二种*/
return this.flat(Infinity);
/*第三种*/
return this.toString().split(',').map(item=>{
return Number(item);
});
/*第四种*/
let arr=this.slice(0);
while (arr.some(item => Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
/*第五种*/
let result = [];
let fn = function (ary) {
for (let i = 0; i < ary.length; i++) {
let item = ary[i];
if (Array.isArray(ary[i])) {
fn(item);
} else {
result.push(item);
}
}
}
fn(this);
return result;
}
function unique(){
/*第一种*/
return Array.from(new Set(this));
/*第二种*/
return [...new Set(this)];
/*第三种*/
let obj={};
for(let i=0;i<this.length;i++){
let item=this[i];
if(typeof obj[item]!=='undefined'){
this[i]=this[this.length-1];
this.length--;
i--;
continue;
}
obj[item]=item;
}
return this;
}
Array.prototype.flatten=flatten;
Array.prototype.unique=unique;
}();
let arr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9,[11, 12, [12,
13, [14]]]], 10];
ary.flatten().unique().sort((a,b)=>a-b); //=>[1, 2, 3, 4, 5,
6, 7, 8, 9....]