# 1、[] == ![] 为什么是 true?

转化步骤:

1、!运算符优先级最高,![]会被转为为 false,因此表达式变成了:[] == false
2、根据上面第(4)条规则,如果有一方是 boolean,就把 boolean 转为 number,因此表达式变成了:[] == 0
3、根据上面第(5)条规则,把数组转为原始类型,调用数组的 toString()方法,[]转为空字符串,因此表达式变成了:'' == 0
4、根据上面第(3)条规则,两边数据类型为 string 和 number,把空字符串转为 0,因此表达式变成了:0 == 0
5、两边数据类型相同,0==0 为 true

# 2、作用域和闭包

变量的作用域
1、全局变量作用域:顶层定义的变量,所有函数都可以调用,作用范围是全局的。(关键点在于在什么地方被调用)
2、局部变量作用域:在函数内部定义变量,只有在函数内部才可以使用,作用范围只是在函数内部。(关键点在于在什么地方被定义)
作用域的特点
次序性和向上线程性搜索

闭包涵义
闭包就是能够读取其他函数内部变量的函数

闭包作用
突破链式作用域的限制,将函数内部的变量和方法传递到外部。

闭包的优点
1、可以读取函数内部的变量;
2、让这些变量的值始终保持在内存中(也就是产生数据缓存,提高效率)。

闭包的注意点
1、由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题
2、闭包会在父函数外部,改变父函数内部变量的值。所以在使用的时候一定要注意


function name(x) {
      let a = 10 ;
      addNot=function(){
       a= a+x;
      }
      function name1() {
        return a
      }
      return name1;
    }
    const result=name(1);
    
    /* 
    闭包突破链式作用域的限制导致我们能拿到内部函数name声明的a的值
    */
    console.log(result()); // 10

    /* 
    这里可以执行addNot的原因是我在内部函数里面未声明,变量提升了,
    addNot作为了全局变量
    */
    addNot();

    /* 
    这里得到11的原因是因为,result在执行完毕后里面的变量未得到清除,一直存在于缓存
    而且在执行完addNot后里面的a的值已经被修改,这里就要注意了闭包是可以使外部函数
    可修改内部函数的变量
    */
    console.log(result()); // 11

# 3、JavaScript执行机制

首先我们需要深知JavaScript是一门单线程语言,一切的多线程都是由单线程模拟出来的,而在HTML5中提出了Web-Worker其实本质还是单线程

# JavaScript事件循环

单线程的js,就像是单行道的车辆,必须一辆一辆的通过,如果有一辆车行走的很慢,那么后面的车辆就必须等待。这样在我们前端上就会出现页面卡住或者白屏情况。
为解决这样的问题我们采用了同步/异步任务

简单的分类

同步任务:耗时少,界面骨架、界面元素渲染
异步任务:耗时久,多为资源占比大的数据和图片

任务执行机制

foo

在JavaScript中比较典型的实例就是Ajax的执行,(Promise、setTimeout、setInterval)

let data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('发送成功!');
    }
})
console.log('代码执行结束'); 

打印结果

1、代码执行结束
2、发送成功

执行说明
1、ajax进入Event Table,注册回调函数success。
2、执行console.log('代码执行结束')。
3、ajax事件完成,回调函数success进入Event Queue。
4、主线程从Event Queue读取回调函数success并执行。

# 任务分类

在同步/异步任务中,我们还将任务更加细致的划分成
宏任务 :大的代码结构体,setTimeout、setInterval、ajax
微任务 :Promise

进入整体代码(宏任务)后,开始第一次循环。接着执行所有的微任务。然后再次从宏任务开始,找到其中一个任务队列执行完毕,再执行所有的微任务。
foo

# 总结

  1. 一般的JavaScript代码(同步)的属于宏任务,定时器相关的异步代码,包括setTimeOut、setInterval等也属于宏任务,promise、 process.nextTick属于微任务;
  2. 同步的代码会按照执行顺序顺序执行,遇到异步代码的时候,属于宏任务的放到宏队列,微任务放到微队列,其中promise需要resolve或者reject才会执行then或者catch里面的内容,其他的放到队列的属于回调函数的内容。
  3. 执行顺序是宏任务-微任务-宏任务……,因为整个脚本就是一个宏任务,所以当里面宏任务和微任务同时放入队列,会先执行玩微任务再执行宏任务;前提是代码执行完毕,如果存在嵌套关系,则会先执行完该任务再执行下一个任务,

# 4、拷贝探究

首先我们来分析一下拷贝的区别

foo

# JavaScript 数据类型

不同的数据类型拷贝的结果不一样

# 一、基本数据类型(栈)

1、undefined,
2、boolean,
3、number,
4、string,
5、null。

在基本数据类型中拷贝数据的值是不可变的,动态修改了基本数据类型的值,它的原始值也是不会改变的

存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配,是直接按值存放的,所以可以直接访问

        let a = 2;
        let b = a;
        b = 3;
        console.log(b, a);
        // 3,2

# 二、引用数据类型(堆)

1、object
2、function
3、data
4、array
5、map

在引用数据类型中拷贝数据的值是可变的,动态修改值的时候会出现两种情况(浅拷贝和深拷贝);

# 浅拷贝

        let a = [0, 1, 2, 3, 4],
          b = a;
        console.log(a === b);
         // true 
        b[0] = 1;
        console.log(a, b);
       // [1, 1, 2, 3, 4] , [1, 1, 2, 3, 4]

浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。

# 导致原因:

而对于对象这种内存占用比较大的来说,直接让复制的东西等于要复制的,那么就会发生引用,因为这种复制,只是将复制出来的东西的指向指向了要复制的那个东西,简单的说,就是两个都同时指向了一个空间,如果改变其中一个,另一个也会发生变化。这就发生了引用。

引用数据类型--名存在栈内存中,值存在于堆内存中,但是栈内存会提供一个引用的地址指向堆内存中的值

foo

# 深拷贝

深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。

相对于浅拷贝就是解除两个对象的相互影响

# 实现方式:

1、JSON.parse(JSON.stringify())
2、函数库lodash的_.cloneDeep方法
3、jQuery.extend()方法
4、手写递归方法

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。

function deepClone(obj){
    let objClone = Array.isArray(obj)?[]:{};
    if(obj && typeof obj==="object"){
        for(key in obj){
            if(obj.hasOwnProperty(key)){
                //判断ojb子元素是否为对象,如果是,递归复制
                if(obj[key]&&typeof obj[key] ==="object"){
                    objClone[key] = deepClone(obj[key]);
                }else{
                    //如果不是,简单复制
                    objClone[key] = obj[key];
                }
            }
        }
    }
    return objClone;
}    
let a=[1,2,3,4],
    b=deepClone(a);
a[0]=2;
console.log(a,b);

# 5、关于this指向的解析

我们先解析一下this的四种基本指向

# 第一种指向关系:

如果函数做为方法在全局时,this指向则默认绑定
在非严格模式下,this指向的是全局对象window
在严格模式下,this指向是undefined

// 例1
  // 在采用var声明的时候,浏览器默认编译的ES5语法,是非严格模式
    var data = {
      a:1,
      b:2,
      c:3
    }
    function name() {
      console.log(this.data.a); // 1
      return this.data.a+this.data.b+this.data.c
    }
    name();
 // 例2
    // 采用const或let声明的时候,浏览器默认ES6语法,则处于严格模式
    // 严格模式下this指向是绑定在undefined上的
    const data = {
      a:1,
      b:2,
      c:3
    }
    function name() { 
      console.log(this.data.a); // undefined
      return this.data.a+this.data.b+this.data.c
    }
   console.log(name());

# 第二种指向关系:

如果函数是在某个上下文对象中调用,this指向的就是那个上下文的对象
这就是典型的隐式绑定,当函数引用有上下文对象时,this都会被隐式绑定
到当前这个上下文对象

    // 例1
        const age = {
            a:100,
            fn1:function () {
            console.log(this.a);
            }
        }
        age.fn1(); // 100
// 例2
function name() {
    console.log(this.a);
  }
  /* 
    出现undefined的原因是age这个对象还未对函数进行引用,隐式绑定未绑定到
    这个age上下文对象,this的对象指向还是全局对象window
  */
  console.log(name()); // undefined
  const age = {
    a:100,
    fn1:name
  }
  age.fn1(); // 100 

# 第三种指向关系:

如果函数是在new中调用时,this指向的就是new的那个对象

const name = new fn(1); 
  new fn = function(a) {
      console.log(this); // fn
      this.b = a+1;
      console.log(this.b);  // 2
      return this.b;
  }

# 第四种指向关系

如果做为一个函数的方法调用函数,那么this指向就需要分情况讨论了

/* 
  情况一、匿名函数调用
  声明即调用,this指向依旧是window对象
  */
  (function () {
    console.log(this);
  })()
/* 
  情况二、call、apply函数调用
  典型的显式绑定,this指向绑定的中指定的对象
  在JavaScript中apply和call传入的第一个参数都是指定的this值,它们的区别在于第二个参数,
  apply的第二个参数传入的值是一个数组或对象
  call的第二个参数传入的值可以是多个数组或对象
  */
  const a ={
        name : "heyuanpeng",
        fn : function (x,y) {
          console.log(this.name); //  heyuanpeng
          console.log( x+y) // 3
        }
    }
    const b = a.fn;
    b.apply(a,[1,2])  
    
    b.call(a,1,2)

注意:
在我们上面这么多实例中,会发现大体的一个规则就是this 永远指向最后调用它的那个对象

# 如何去改变一个this的指向呢

方法一、使用箭头函数

箭头函数的this始终指向函数定义时的 this,而非执行时

方法二、在函数内部使用that=this

方法三、使用call、apply、bind方法传入指定的this对象

# 6、跨域问题

受浏览器同源策略限制的请求场景

同源策略(SOP),同源是指“协议+域名+端口”相同;

# 跨域的解决方案:

# 1、jsonp

创建动态的script,再请求一个带参数的网络地址实现跨域通信

# 2、iframe

1、 document.domain + iframe跨域
此方案仅限主域相同,子域不同的跨域应用场景。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
2、location.hash + iframe跨域
实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
3、window.name + iframe跨域
通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

# 3、postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
a.) 页面和其打开的新窗口的数据传递
b.) 多窗口之间消息传递
c.) 页面与嵌套的iframe消息传递
用法:postMessage(data,origin)方法接受两个参数
data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

# 4、跨域资源共享(CORS) :主流的跨域解决方案

普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。前端需要在XMLHttpRequest中设置cookieDomainRewrite:true,允许携带cookie。

# 5、nginx代理跨域

跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

server {
    listen       10000;  
    server_name  localhost;

    location / {
        proxy_pass   http://www.baidu.com:8088;  #反向代理
         
    }
}

# 6、WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。

WebSocket的优点:
1、支持双向通信,实时性更强(可以用于做实时通信)
2、更好的二进制支持
3、较少的控制开销(连接创建后,ws客户端、服务端进行数据交换时,协议控制的数据包头部较少)


<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>