JavaScript篇

谈谈你对深拷贝和浅拷贝的理解?

一般我们使用 赋值表达式 =,Object.assign,展开运算符(…)都属于浅拷贝,拷贝对面里面的元素和数据源的地址是相同的,都指向一片空间。如果你不小心修改了拷贝对象里面的元素,后果就是原对象的内容也修改了。

而深拷贝,我们一般用 JSON.parse(JSON.stringify(obj)) 或者 lodash.cloneDeep()的方式去拷贝。

JSON.parse(JSON.stringify(obj))的缺点:

如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式。而不是时间对象;
如果obj里有RegExp、Error对象,则序列化的结果将只得到空对象;
如果obj里有function,Symbol 类型,undefined,则序列化的结果会把函数或 undefined丢失;
如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null
JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor

loadsh.cloneDeep()是可以clone方法的

对JS的slice和splice的理解?

slice是用来截取,比如slice(1,3),下标含头不含尾的截取,slice(1),从下标1开始截取到最后,slice(-2)表示截取倒数第二个开始,取到最后。返回被提取的数组。原数组不变。

splice通过删除或替换现有元素或者原地添加新的元素来修改数组,原数组变化。

spilce(1,0,'a','b'),在下标为1的位置开始删除0个元素,追加两个元素'a','b',此时'a'的下标变成1

spilce(1,1,'a','b'),在下标为1的位置开始删除1个元素,追加两个元素'a','b',此时'a'的下标变成1

返回被删除的数组

对JS的apply,call,bind的理解?

bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。

call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。

javaScript 的一大特点是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。

function fruits() {}
 
fruits.prototype = {
    color: "red",
    say: function() {
        console.log("My color is " + this.color);
    }
}
 
var apple = new fruits;
apple.say();    //My color is red

但是如果我们有一个对象banana= {color : "yellow"} ,我们不想对它重新定义 say 方法,那么我们可以通过 call 或 apply 用 apple 的 say 方法:

banana = {
    color: "yellow"
}
apple.say.call(banana);     //My color is yellow
apple.say.apply(banana);    //My color is yellow

对于 applycall 二者而言,作用完全一样,只是接受参数的方式不太一样。call按顺序传参,apply接收数组

func.call(this, arg1, arg2);
func.apply(this, [arg1, arg2])
Math.max.apply(Math, [5, 458 , 120 , -215 ])
Math.max.call(Math,5, 458 , 120 , -215)

bind()最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的this值。常见的错误就像上面的例子一样,将方法从对象中拿出来,然后调用,并且希望this指向原来的对象。如果不做特殊处理,一般会丢失原来的对象。使用bind()方法能够很漂亮的解决这个问题:

this.num = 9; 
var mymodule = {
  num: 81,
  getNum: function() { 
    console.log(this.num);
  }
};

mymodule.getNum(); // 81

var getNum = mymodule.getNum;
getNum(); // 9, 因为在这个例子中,"this"指向全局对象

var boundGetNum = getNum.bind(mymodule);
boundGetNum(); // 81

 MDN的解释是:bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

var foo = {
    bar : 1,
    eventBind: function(){
        $('.someClass').on('click',function(event) {
            /* Act on the event */
            console.log(this.bar);      //1
            // 如果不用bind,this将指向对应class的那个元素对象
        }.bind(this));
    }
}
(function fn(a, b, c) {
  return a + b + c;
}).bind(null, 10, 20)(5)
// 35
// 转成了一个新函数,a和b已经和10,20绑定了,5传递给了这个新函数的剩余参数

JS里对this的理解

this永远指向函数运行时所在的对象,而不是函数被创建时所在的对象。

  • 普通的函数调用,函数被谁调用,this就是谁。
  • 构造函数的话,如果不用new操作符而直接调用,那即this指向window。用new操作符生成对象实例后,this就指向了新生成的对象。
  • 匿名函数或不处于任何对象中的函数指向window 。
  • 如果是call,apply等,指定的this是谁,就是谁。

事件冒泡,事件捕获,阻止默认行为

事件冒泡,事件捕获:

我们可以在addEventListener的最后一个参数设置是冒泡还是捕获。

【【【div1】div2】div3】

假如有三个div,从里到外是div1,div2,div3。我们给三个div都加上一个点击事件。当点击div1, 则从冒泡顺序是div1->div2->div3(从内到外),而捕获顺序是div3->div2->div1(从外到内)

如果同时定义了冒泡和捕获,则触发顺序是先捕获再冒泡,即先从外到内,再从内到外。

focus,blur,change,submit,reset,select等不参与冒泡

event.stopPropagation() 阻止冒泡,在div1加入这行代码,冒泡到这一层就不会再往下继续了。

默认行为:

如表单提交,a标签跳转,右键鼠标菜单

以标签a为例,当点击a标签时,默认都是跳转的动作,这个就是默认事件;如果给a加一个click事件,并且只想执行click,则需要在里面加上event.preventdefault(),此时默认跳转行为会被阻止,只执行点击事件。

假如div3是a标签,点击a标签,实际干了三件事情,先后为 执行本身处理函数比如click事件、冒泡父级处理函数,接着响应默认事件(跳转)

如果想只处理a的自身函数,其它都不处理。

方法一:可以在a事件加上event.stopPropagation()和event.preventdefault()

方法二:在div2事件里写上return false

原生对象和宿主对象

原生对象:JS内置的对象,比如Array,Date

宿主对象:由宿主环境决定,如果在浏览器运行,会有window及其子对象document,location等,如果在node环境运行,则有globla及其子对象

理解下面代码的区别

function foo(){ //code }()

以function关键字开头的语句会被解析为函数声明,而函数声明是不允许直接运行的。 只有当解析器把这句话解析为函数表达式,才能够直接运行

(function foo(){ // code.. })()

运算符开头的表达式被立即执行

JavaScript的同源策略

所谓"同源"指的是"三个相同",协议+域名+端口

同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。因此A网页设置的 Cookie,B网页不能读到,除非这两个网页"同源"。比如恶意网站的页面通过iframe嵌入了银行的登录页面(二者不同源),因此恶意网站得不到用户输入的任何信息和cookie。

如果非同源,共有三种行为受到限制:

(1) Cookie、LocalStorage 和 IndexDB 无法读取。

(2) DOM 无法获得。

(3) AJAX 请求不能发送。

同源政策规定,AJAX请求只能发给同源的网址,否则就报错。

有三种方法规避这个限制。

  • JSONP
  • WebSocket
  • CORS
  • Nginx代理

跨域技术-CORS,是 HTML5 的一项特性,它定义了一种浏览器和服务器交互的方式来确定是否允许跨域请求。

前端正常ajax请求:

$.ajax({
  type: "post",
  url: 'http://192.168.45.152:8081/conference/user/bind',
  async: false, // 使用同步方式
  // 1 需要使用JSON.stringify 否则格式为 a=2&b=3&now=14...
  // 2 需要强制类型转换,否则格式为 {"a":"2","b":"3"}
  data: JSON.stringify({
    a: 1,
    b: '2',
    now: new Date().getTime() // 注意不要在此行增加逗号
  }),
  headers: {
    'Authentication': 'xxxxxx'
  },
  contentType: "text/plain",
  dataType: "json",
  success: function (data) {
    console.log(data)
  } // 注意不要在此行增加逗号
});

后端配置允许跨域的域名,路径,方法等:

@Configuration
public class CorsConfig implements WebMvcConfigurer {
  @Override
  public void addCorsMappings(CorsRegistry registry) {
    //设置允许跨域的路径
    registry.addMapping("/**")
      //设置允许跨域请求的域名
      .allowedOrigins("*")
      //是否允许证书 不再默认开启
      .allowCredentials(true)
      //设置允许的方法
      .allowedMethods("GET", "POST")
      //跨域允许时间
      .maxAge(3600);
  }
}

Nginx代理:

项目部署的时候,用nginx服务起做代理:

80端口: 请求端口,所有请求到此处

8080端口: API服务器端口,通过请求的路径匹配代理进入

JSONP的工作原理

jsonp是一种跨域通信的手段,它的原理其实很简单:

  1. 首先是利用script标签的src属性来实现跨域。

  2. 通过将前端方法作为参数传递到服务器端,然后由服务器端注入参数之后再返回,实现服务器端向客户端通信。

  3. 由于使用script标签的src属性,因此只支持get方法

封装一个简单的JSONP

(function (global) {
    var id = 0,
        container = document.getElementsByTagName("head")[0];

    function jsonp(options) {
        if(!options || !options.url) return;

        var scriptNode = document.createElement("script"),
            data = options.data || {},
            url = options.url,
            callback = options.callback,
            fnName = "jsonp" + id++;

        // 添加回调函数
        data["callback"] = fnName;

        // 拼接url
        var params = [];
        for (var key in data) {
            params.push(encodeURIComponent(key) + "=" + encodeURIComponent(data[key]));
        }
        url = url.indexOf("?") > 0 ? (url + "&") : (url + "?");
        url += params.join("&");
        scriptNode.src = url;

        // 传递的是一个匿名的回调函数,要执行的话,暴露为一个全局方法
        global[fnName] = function (ret) {
            callback && callback(ret);
            container.removeChild(scriptNode);
            delete global[fnName];
        }

        // 出错处理
        scriptNode.onerror = function () {
            callback && callback({error:"error"});
            container.removeChild(scriptNode);
            global[fnName] && delete global[fnName];
        }

        scriptNode.type = "text/javascript";
        container.appendChild(scriptNode)
    }

    global.jsonp = jsonp;

})(this);

调用

jsonp({
    url : "www.example.com",
    data : {id : 1},
    callback : function (ret) {
        console.log(ret);
    }
});

为什么会跨域,怎么解决 

浏览器出于安全考虑,采用了同源策略(参考上面)。

浏览器从一个域名的网页去请求另一个域名的资源时,域名、端口、协议任一不同,都是跨域。

对于前后端分离项目:

1. 假如前端代码和后端代码部署在一台服务器,可以通过nginx做代理来规避跨域

2. 假如前端代码和后端代码分别部署在不同服务器,我们在一个网页请求另一个网页数据是不允许的。此时可以在服务端设置allow-Origin。

CORS跨域请求的简单请求非简单请求

同时满足两大条件,就属于简单请求

  • 请求方式:GET、POST、HEAD(注:什么是HEAD请求?HEAD请求和GET本质是一样的,但是HEAD请求不含数据,只有HTTP头部信息)
  • HTTP头部信息不超过一下几种字段:无自定义头部字段、Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type(只有三个值application/x-www-form-urlencoded、multipart/form-data、text/plain)

非简单请求,符合下面任意一条

  • 请求方式:PUT、DELETE
  • 自定义头部字段
  • 发送json格式数据
  • 正式通信之前,浏览器会先发送OPTION请求,进行预检,这一次的请求称为“预检请求”
  • 服务器成功响应预检请求后,才会发送真正的请求,并且携带真实数据

服务端的"OPTION-预检"里边一定要包含允许客户端使用非简单方式请求数据的响应头,如果预检失败会返回 Origin http://XXX.com is not allowed by Access-Control-Allow-Origin.

否则会返回如下:

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000

一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段

什么是回调地狱

一个异步请求套着一个异步请求,一个异步请求依赖于另一个的执行结果,使用回调的方式相互嵌套。

Promise解决了这个问题

async/wait在Promise的基础上优化,同步写法,异步执行,使代码看起来更简洁。

javascript es6箭头函数相对于普通的函数有什么区别

ES6 箭头函数和普通函数之间有几个关键区别:

  1. 语法简洁: 箭头函数提供了更简洁的语法形式。它们可以使用箭头(=>)来定义函数,省略了普通函数中的function关键字和大括号。如果函数体只有一条表达式,箭头函数会自动将该表达式的结果作为返回值。

  2. 无绑定的this: 箭头函数没有自己的this值,它会继承外层作用域中的this值。这种行为与普通函数不同,普通函数的this值在运行时根据调用方式来确定。箭头函数的继承机制方便了对上下文的引用,特别是在嵌套函数和回调函数中,可以避免this指向的困扰。

  3. 没有arguments对象: 箭头函数没有自己的arguments对象,它会继承外层作用域中的arguments对象。如果需要访问传递给箭头函数的参数,可以通过扩展运算符(...)或使用默认参数的方式来获取参数值。

  4. 不能作为构造函数: 箭头函数不能用作构造函数,因此不能使用new关键字实例化一个箭头函数。普通函数可以作为构造函数使用,并创建一个新的对象实例。

  5. 没有原型: 箭头函数没有自己的prototype属性,因此无法通过new关键字来扩展方法或属性。普通函数可以通过原型链来继承和扩展。

需要注意的是,由于箭头函数具有不同的语法和行为,它们并不完全取代普通函数。在特定情况下,普通函数可能更适合实现某些功能,特别是需要动态绑定this值或使用构造函数的情况下。因此,在选择使用箭头函数或普通函数时,需要根据具体的需求和语境进行权衡和选择。