【面试题】2024前端高频面试题

文章目录

1,防抖和节流

我们知道,函数执行,形成一个不销毁的私有作用域,能够保护里面的变量和外界互不干扰,减少全局变量的污染,以上就是闭包的基础概念,在真实项目中,由于闭包是形成一个不销毁的栈内存,所以我们应减少对他的使用,但是有些情况下,我们对于闭包的应用还是比较重要的,比如:

  • 1,在做循环事件绑定的时候,在没有let的情况下,基本上都是基于闭包来存储循环的索引值的
  • 2,还有一个关于闭包的应用是柯里化编程思想,尤其是Function原型上有个叫bind方法,它是预先处理this指向的一个机制,其实就是用柯里化来做的,在项目中我们用它处理过函数的防抖和节流,其实也是用闭包的思想利用柯里化函数的思想来实现的
1,防抖
1, 定义

函数的防抖(debounce):在某个间隔时间内只执行一次,避免函数过多的执行

2,手写防抖
/**
   * debounce: 函数防抖
   *  @params
   *    func: 要执行的函数
   *    wait: 间隔等待时间
   *    immediate: 在开始边界还是结束边界触发执行(true: 在开始边界)
   *  @return
   *    可被调用的函数
   * **/
  function debounce(func, wait, immediate) {
    // func:要触发的函数
    // wait: 等待的时间
    // immediate: 是在开始边界触发,还是在结束边界触发 => 事件一触发的时候就把这函数执行,还是在这个当前时间之内等待时间已经结束了才把这个函数执行
    let result = null, // 用来接受函数执行返回的结果
      timer = null;
    return function (...arg) {
      // 柯理化思想(闭包思想) 不管是防抖还是节流都是柯里化的一种应用,也叫闭包的应用
      let context = this,
        now = immediate && !timer; // now: 是否现在就执行
      clearTimeout(timer); // => 这句是防抖的关键,除去这句根本不是防抖,只是在没防抖基础上延缓了函数执行的次数,加了这句,在设置新的定时器之前我们要把之前设置的定时器都给干掉,因为防抖的目的是:等待时间内只执行一次
      timer = setTimeout(() => {
        // => 如果不是now,设置个定时器来做
        if (!immediate) result = func.call(context, ...arg);
        clearTimeout(timer); // => 这里清不清都行,上面已经清除过了
        timer = null;
      }, wait);
      if (now) result = func.call(context, ...arg); //if now立即执行,就让这个方法func立即执行(if now是立即执行的,就让func()函数执行,并改变func方法中的this)
      return result;
    };
  }
  let count = 0;
  function fn() {
    console.log(++count);
  }
  let lazyFn = debounce(fn, 100, true); // => 所以返回结果是可被执行的函数
  window.onscroll = lazyFn; // => lazyFn是函数

  // => 设为true是在开始边界触发,设为false是在结束边界触发
  // => 传true在开始边界触发,设为false可以不写
  // let lazyFn = debounce(fn, 100);
2,节流
1,定义

函数节流(throttle):是为了缩减频率,当达到了一定的时间间隔就会执行一次。

  • 防抖是控制在一定时间之内只能执行一次
  • 节流是在一定时间之内可以执行多次,但是要把之前的执行频率缩减一些而已
2,手写节流
/**
 * throttle: 函数节流
 *  @params
 *    func: 需要执行的函数
 *    wait: 设置的时间间隔
 *  @return
 *    返回可被调用的函数
 * **/
let throttle = function (func, wait) {
  let timer = null,
    result = null,
    previous = 0; // => 上次执行时间点
  return function (...arg) {
    let now = new Date(),
      context = this;
    let remaining = wait - (now - previous); // remaining <= 0,表示上次执行至此所间隔时间已经超过一个时间间隔
    // now - previous 间隔的时间(上一次触发距离现在触发间隔的时间) 
    if (remaining <= 0) {  // <= 0  => 等待时间wait - 时间间隔 <= 0: 说明过了等待时间了
      // 过了等待时间需要立即执行函数,同时让当前执行的时间作为下一次的上一次时间
      clearTimeout(timer);
      previous = now;
      timer = null;
      result = func.apply(context, arg);
    } else if (!timer) {  // => 在等待时间间隔内
      timer = setTimeout(() => {
        previous = new Date();
        timer = null;
        result = func.apply(context, arg);
      }, remaining);
    }
    return result;
  };
};

// => 这里没写第三个参数,underscore里有写,可以看看它写的
3,扩展:一个常用的JS类库underscore

https://underscorejs.org/ : underscore是一个JavaScript库,它提供了一大堆有用的函数

// => 安装
cnpm install underscore
// => 使用, 以防抖为例
<script src="./_underscore@1.13.6@underscore/underscore.js"></script>
let count = 0
function fn () {
  console.log(++count)
}
let lazyFn = _.debounce(fn, 1000)
window.onscroll = lazyFn
4,使用场景
  • 瀑布流效果使用节流更好一些

  • 下拉图片延时加载用节流

  • 滚动到底部加载更多数据用防抖:因为每次操作只执行一次

5,防抖和节流的区别

防抖是每一次事件操作的时候,就算一直在执行,也会等到操作开始或者操作结束之后执行一次

节流是在一段时间之内操作,设定一个间隔时间,把它执行的频率减少就行了

防抖的关键在于设置新的定时器之前,要把之前设置的定时器都干掉,而节流不清,到达间隔时间以外的立即执行,没有到达间隔时间的,设置一个定时器等待到达这个时间再执行,只是把频率给减少了

2,var、let、const区别

1,var
1, 声明提升
console.log(num) // undefined
var num = 100

2, 变量覆盖
var num1 = 100
var num1 = 200
conosle.log(num1) // 200

3, 没有块级作用域
function fn() {
  for (var i = 0; i <= 5; i++) {
    console.log(i)  // 0 1 2 3 4 5
  }
  console.log(i)  // 6
}
fn()
2,let
1, let 声明的变量具有块级作用域的特征
function fn() {
  let a = 10
}
console.log(a) // a is not defined

2, 在同一个块级作用域,不能重复声明变量
function fn() {
  let a = 10
  let a = 20
}
console.log(a) // Identifier 'a' has already been declared

3, let 声明的变量不存在变量提升,换种说法就是let声明存在暂时性死区(TDZ)
console.log(num) // Cannot access 'num' before initialization
let num = 10

3,const
1, const 声明的变量具有块级作用域的特征
function fn() {
  const a = 10
}
console.log(a) // a is not defined

2, 在同一个块级作用域,不能重复声明变量
function fn() {
  const a = 10
  const a = 20
}
console.log(a) //  Identifier 'a' has already been declared

3, const 声明的变量不存在变量提升,换种说法就是const声明存在暂时性死区(TDZ)
console.log(num) // Cannot access 'num' before initialization
const num = 10

4, const 声明一旦定义后,不能修改
const num = 100
num = 200
console.log(num) // Assignment to constant variable.
4,块级作用域

ES5中作用域有:全局作用域、函数作用域,没有块级作用域的概念。

ES6新增了块级作用域,由 {} 包括,if 语句for 语句里面的 {} 都属于块作用域

{
  var num = 100
  console.log(num) // 100
}
console.log(num) // 100 可见,通过var定义的变量可以跨块作用域访问到

function fn() {
  var num = 100
  console.log(num) // 100
}
console.log(num) // num is not defined 可见,通过var定义的变量不能跨函数作用域访问到

if (true) {
  var num = 100
}
console.log(num) // 100

for (var i = 0; i <= 5; i++) {
  var num = 100
}
console.log(i) // 6
console.log(num) // 100
if 语句和 for 语句中用 var 定义的变量可以在外面访问到,可见,if 语句和 for 语句属于块级作用域,不属于函数作用域
5,三者区别
  • var 定义的变量,没有块的概念,可以跨块访问, 不能跨函数访问
  • let 定义的变量,只能在块作用域里访问,不能跨块访问,也不能跨函数访问
  • const 用来定义常量,使用时必须初始化(即必须赋值),只能在块作用域里访问,且不能修改
// 块级作用域
{
  var a = 100
  var b = 200
  var c = 300
  var a1
  let b1
  // const c1
  console.log(a) // 100
  console.log(b) // 200
  console.log(c) // 300
  console.log(a1) // undefined
  console.log(b1) // undefined
}
console.log(a) // 100
console.log(b) // 200
console.log(c) // 300

// 函数作用域
(function fn() {
  var a = 100
  let b = 200
  const c = 300
  console.log(a) // 100
  console.log(b) // 200
  console.log(c) // 300
})()
console.log(a) // a is not defined
console.log(b) // b is not defined
console.log(c) // c is not defined
6,const 定义的变量可以修改吗?

const 定义的基本类型不能改变,但是定义的对象是可以通过修改对象属性等方法改变的

const num = 100
num = 200
console.log(num) // Assignment to constant variable.

const person = {
  name: 'Tom',
  age: 18
}
person = {
  name: 'Jerry',
  age: 20
}
console.log(person) // Assignment to constant variable.

const person = {
  name: 'Tom',
  age: 18
}
person.name = 'Jerry'
console.log(person) // {name: 'Jerry', age: 18}

基本数据类型的变量保存在栈区中,基本数据类型的值直接在栈内存中存储,值与值之间是独立存在的,修改一个变量不会影响其他的变量。

引用数据类型的值是同时保存在栈内存和堆内存的对象,栈区保存了对象在堆区的地址。

const 声明的 person 给属性重新赋值是可以的,但是给 person 重新赋值是不可以的,那样会改变 person 在栈区的地址

7,经典面试题
for (var i = 0; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}
console.log(i)

setTimeout 是异步执行的,1000ms后向任务队列里添加一个任务,只有主线程的全部执行完才会执行任务队列里的任务,所以当主线程 for 循环执行完之后 i 的值为 6, 这时候再去执行任务队列里执行任务, i 全部是 6。

每次 for 循环的时候 setTimeout 都会执行,但是里面的 function 则不会执行被放入任务队列,因此放了 6 次, for 循环的 6 次执行完之后不到 1000ms, 100ms后全部执行任务队列里的函数,所以输出 6 个 6。

for (let i = 0; i <= 5; i++) {
  setTimeout(function() {
    console.log(i)
  }, 1000)
}
console.log(i)

let定义的 i 事块级作用域,每个 i 只能存活到大括号结束,并不会把后面的 for 循环的 i 值赋给前面的 setTimeout 中的 i,而var 定义的 i是局部变量,这个 i 的生命周期不受for 循环的大括号限制。

3,new操作符做了什么

// => 先来做道题
function Fn(n) {
  let m = 10;
  this.total = n + m;
  this.say = function () {
    console.log(this.total);
  };
}
let f1 = new Fn(10);
let f2 = new Fn(20);
let f3 = new Fn();
console.log(f1.m);
console.log(f2.n);
console.log(f1.total);
f2.say();
console.log(f1 === f2);
function Fn() {
  this.x = 100;
  this.y = 200;
}
let f1 = new Fn();
let f2 = new Fn();

/**
   * new 执行也会把类当作普通函数执行(当然也有类执行的一面)
   *  1,创建一个私有的栈内存
   *  2,形参赋值 & 变量提升
   *  3,浏览器创建一个对象出来(这个对象就是当前类的一个新实例),并且让函数中的this指向这个实例对象 => 构造函数模式中,方法中的 this 是当前类的实例
   *  4,代码执行
   *  5,在我们不设置return的情况下,浏览器会把创建的实例对象默认返回
   * **/

创建Fn的新实例,必须使用new操作符,以这种方式调用构造函数实际上会经历以下4个步骤(即new操作符做了什么):

  • 1,创建一个新对象o
  • 2,将构造函数的作用域赋值给新对象,因此this就指向了这个新对象o
  • 3,执行构造函数中的代码,为这个新对象添加属性
  • 4,返回新对象

4,同源策略和跨域

跨域是一个经常遇到的问题,特别是当服务有两个域名的时候,比如你的页面是在a.test.com,然后需要向b.test.com请求数据的时候,这个时候就跨域了,如果直接请求,就会报如下错误

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

这个时候就要了解什么是同源策略了。

1,同源策略

现在需要向另外的网站请求数据,例如抓取谷歌搜索的结果。然后写这么一个请求, 搜索内容为"hello",如下:

let url = "https://so.csdn.net/so/search?spm=1000.2115.3001.4498&q=htllo";
let req = new XMLHttpRequest();
req.open("GET", url);
req.send();

执行后,浏览器会报错:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

大意是说localhost域名无法向so.csdn.net域名请求数据。

因为同源策略的限制,不同域名,协议(http、https)或者端口号无法直接进行请求。同源策略只针对于浏览器端,浏览器一旦检测到请求的结果的域名不一致,就会堵塞请求结果,这里注意,跨域请求是可以发过去的,但是请求响应response被浏览器堵塞了。如下:

我们本地启两个服务一个8000,一个9000,在8000端口打开一个页面向9000端口的服务发请求:

// 8000客户端
let url = "http://192.168.10.55:9000";
let req = new XMLHttpRequest();
var data = "key1=value1";
req.open("POST", url);
req.send(data);

// 9000 服务端
const http = require("http");
const querystring = require("querystring");
const server = http.createServer((req, res) => {
  if (req.method === "POST") {
    let body = "";

    req.on("data", (chunk) => {
      body += chunk.toString();
    });

    req.on("end", () => {
      const params = querystring.parse(body);
      console.log("收到请求", req.url);
      console.log("cookie", res.cookie);
      console.log("收到数据:", params);
      res.end(JSON.stringify(params));
    });
  } else {
    res.end("send a POST request\n");
  }
});
server.listen(9000, () => {
  console.log("server running at http://127.0.0.1:9000");
});

服务收到了请求,并正常返回数据,但是返回的数据被浏览器堵塞了,返回码也无法得到。所以说同源策略是限制了不同源的读,但不限制不同源的写。为什么不直接限制写呢?在回答这个问题之前,我们先了解一下同源策略的作用:

假设我们打开了A银行http://Abank.com,已经通过了登录验证,然后再打开另一个黑网站http://evil.com,这个网址刚好是抓使用Aback.com的肉鸡,在evil.com的代码里会向Abank.com发请求,例如转账请求,将余额转到自己的账户,但是由于同源策略的限制,这种做法无法成功,因为evil.com无法获取我们在Abank.com的信息,包括验证身份的信息(通常是按照一定规则生成的无法猜到的随机token字符串)。token可能放在cookie里,从evil.com向Abank发请求的时候,是不会带上Abank的cookie的,同时也不会带上evil的cookie,虽然cookie是和域名绑定的。由于没有正确的token值,导致无法通过服务的身份验证。

为验证没带cookie,上面的例子,localhost向server.com请求数据,服务将收到的cookie打印出来是undefined,如图:

但是localhost已经成功设置了cookie:

server.com也有设置cookie,如下:

如果要带上cookie的话设置xhr的withCredentials属性为true

回到上面的问题,为什么不限制写呢?因为如果连请求都发不出去,那在源头上就限制死了,网站之间就无法共享资源了。另外,限制读即浏览器拦截请求结果,一般情况下就够了,一方面如果访问的是黑网站,那么网站无法根据请求结果继续下一步的操作,如不断的猜测密码。另一方面如果访问的是白网站,block掉请求结果,应该是考虑到了请求结果可能会使页面重定向,或者是给网页添加一个恶意的iframe之类的。

有什么办法可以绕过同源策略呢?有一个办法就是csrf攻击

2,CSRF攻击

如上面的例子,由于同源策略的限制,默认跨域的ajax请求不会携带cookie,然后script/iframe/img等标签却是支持跨越的,所以在请求的时候会携带上cookie,还是上面的例子,如果登录了Aback.com,那么cookie里就有了token,同时又打开了另外一个标签页访问了evil.com,这个网页里面有一个iframe,如下图:

<iframe src="http://Abank.com/app/money?amount=100000"></iframe>

这个iframe的src是一个Abank.com的转账的请求,如果Abank.com的转装请求没有第二重加密措施的话,那么请求转账就成功了。

第二个例子是路由器的配置,假设我们在网上找了一个路由器的配置教程的网站,这个网站里面偷偷加了一个img标签:

<img src="http://192.168.1.1/admin/config?nexthop=123.45.67.89" style="display: none;" alt="">

192.168.1.1是很多路由器的配置地址。这个图片没有显示出来被忽略了,但是它的请求却发出去了。这个请求给路由器添加了一个vpn代理,指向黑客的代理服务器。如果路由器也是把登录验证放在cookie里,那么这个设置vpn的请求很可能就成功了,以后连接路由器的每个请求都会先经过黑客的服务。

到这里,很明显一个防csrf攻击的策略就是将token添加到请求的参数里,也就是说每个需要验证身份的请求都要显式的带上token值,或者是写的请求不能支持GET。

跨域攻击可以采取一些措施进行规避,但是跨域更多的还是一些世纪的正常应用。

3,解决跨域的方法

跨域请求:有时候在自己的网站需要去请求别的网站的数据时,就需要跨域正常请求。

1,跨域资源共享

CORS是一个W3C标准,全称是"跨域资源共享"

它允许浏览器向跨源服务器发出请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信

浏览器将CORS请求分成两类:简单请求、非简单请求(预检请求)

只要同时满足以下两大条件,就属于简单请求:

(1) 请求方法是以下三种方法之一:
  HEAD
  GET
  POST
(2)HTTP的头信息不超出以下几种字段:
  Accept
  Accept-Language
  Content-Language
  Last-Event-ID
  Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

这是为了兼容表单(form),因为历史上表单一直可以发出跨域请求。AJAX 的跨域设计就是,只要表单可以发,AJAX 就可以直接发。

凡是不同时满足上面两个条件,就属于非简单请求,浏览器对这两种请求的处理,是不一样的。

1,简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段:

GET /cors HTTP/1.1
Origin: http://api.xxx.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0

Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口),服务器根据这个值,决定是否同意这次请求

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段:

Access-Control-Allow-Origin: http://api.xxx.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
/**
	Access-Control-Allow-Origin: 该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求
	Access-Control-Allow-Credentials: 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可
	Access-Control-Expose-Headers: 该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值
*/
2,非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

下面是一段浏览器的JavaScript脚本。

var url = 'http://api.xxx.com/user';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header

浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
/**
Access-Control-Request-Method: 该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
Access-Control-Request-Headers: 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。
*/

预检请求的回应:

服务器收到"预检"请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

Access-Control-Allow-Origin: *

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

XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

服务器回应的其他CORS相关字段如下。

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
/**
Access-Control-Allow-Methods: 该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
Access-Control-Allow-Headers: 如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段
Access-Control-Allow-Credentials: 该字段与简单请求时的含义相同。
Access-Control-Max-Age: 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
*/
2,JSONP

这个方法的原理是浏览器告诉服务器一个回调函数的名称,服务在返回的script里面调用这个回调函数,同时传进客户端需要的数据,这样返回的代码就能在浏览器上执行了。

例如8000端口要向9000端口请求数据,在8000端口的页面定义一个回调函数foo,将foo写在script的src的参数里,这个script标签向9000端口发出请求:

// => 8000 
function addScriptTag(src) {
    var script = document.createElement("script");
    script.setAttribute("type", "text/javascript");
    script.src = src;
    document.body.appendChild(script);
  }

  window.onload = function () {
    addScriptTag("http://192.168.10.55:9000/index.js?callback=foo");
  };

  function foo(data) {
    console.log(data);  // {ip: '8.8.8.8'}
    console.log("Your public IP address is: " + data.ip);
  }

// => 9000
foo({
  "ip": "8.8.8.8"
});

由于它是一个script标签返回的内容,所以它就执行服务返回的script文件了。

这样就实现了跨域的效果。

JSONP和CROS相比,缺点是只支持get类型,无法支持post等其他类型,必须完全信任提供服务的第三方,优点是兼容老版本的浏览器

3,子域跨父域

主域名又称一级域名或者顶级域名,由域名主体.域名后缀组成,比如mysite.com

子域名有二级域名,比如mail.mysite.com,三级域名,比如home.mail.mysite.com

主域相同,子域不同,可以设置document.domain来解决跨域,比如:mail.mysite.com要访问mysite.com的iframe数据,那么在mail.mysite.com脚本里需要执行如下代码:

document.domain = "mysite.com"

这样就可以和父域进行交互了,但是向父域发请求还是会跨域的,因为这种更改domain只支持client side,并不是client to server的

4,iframe

iframe和父窗口也有同源策略的限制,父域无法直接读取不同源的iframe的DOM内容及监听事件,但是iframe可以调用父窗口提供的API。iframe通过window.parent得到父窗口的window对象,然后父窗口定义一个全局对象供iframe调用。

例如在页面通过iframe的方式嵌入一个第三方的视频,如果需要手动播放视频、监听iframe的播放事件,页面需要引入这个第三方提供的视频播放控制API,在这个JS文件里定义了一个全局对象YT:

if (!window['YT']) {
  var YT = {
    loading: 0,
    loaded: 0
  }
}

在视频iframe的脚本里通过window.parent获取得到父窗口即自己网站的页面,代码如下:

sr = new Cq(window.parent, d, b)

自己网站的页面也是在这个YT对象自定义一些东西,如添加播放事件监听,代码如下:

new YT.Player('video', {
  events: {
    'onStateChange': function(data) {
      // do sth
    }
  }
})

这样一旦跨域的子域发生了相关的事件,就可以通过window.parent.YT去调用父域添加的回调函数了,这样就解决了iframe跨域的问题了

5,window.postMessage

在以上4点,父窗口无法向不同源的iframe传递数据,通过window.postMessage可以做到。父窗口向iframe传递一个消息,而iframe监听消息事件

例如在8000端口的页面嵌入一个9000端口的iframe:

<div>
    <input id="text" type="text" value="Runoob" />
    <button id="sendMessage" >发送消息</button>
</div>
<iframe id="receiver" src="http://http://192.168.10.55:9000/index.html" width="300" height="360">
    <p>你的浏览器不支持 iframe。</p>
</iframe>
<script>
window.onload = function() {
    var receiver = document.getElementById('receiver').contentWindow;
    var btn = document.getElementById('sendMessage');
    btn.addEventListener('click', function (e) {
        e.preventDefault();
        var val = document.getElementById('text').value;
        receiver.postMessage("Hello "+val+"!", "http://192.168.10.55:9000");
    });
}
</script>

接收程序有一个事件监听器,监听 “message” 事件,同时我们要验证消息来源地址,以确保是个可信的发送地址。

<div id="recMessage">
Hello World!
</div>
<script>
window.onload = function() {
    var messageEle = document.getElementById('recMessage');
    window.addEventListener('message', function (e) {  // 监听 message 事件
        alert(e.origin);
        if (e.origin !== "http://localhost:8000/") {  // 验证消息来源地址
            return;
        }
        messageEle.innerHTML = "从"+ e.origin +"收到消息: " + e.data;
    });
}
</script>
  • e.source – 消息源,消息的发送窗口/iframe
  • e.origin – 消息源的 URI(可能包含协议、域名和端口),用来验证数据源
  • e.data – 发送过来的数据

5,promise

1,写出以下代码的输出结果
new Promise((resolve, reject) => {
  resolve();
})
  .then()
  .catch((x) => {
    console.log(1);
  })
  .then((x) => {
    console.log(2);
  })
  .then((x) => {
    console.log(3);
  })
  .catch((x) => {
    console.log(4);
  })
  .then((x) => {
    console.log(AAA);
  })
  .catch()
  .then(null, (x) => {
    console.log(5);
  });
2,定义

promise 是ES6语法规范 中新增加的内置类。用来处理JS中异步编程的

console.dir(Promise)

我们可以看出promise是一个function,我们知道所有的类都是function类型的,所有的内置类都是函数数据类型的,函数既可以当作类也可以当作对象,每一个函数都有3个角色:普通函数、类、对象

all、race、reject、resolve就是Promise作为普通函数提供给我们的4个方法

它是内置类,所以用的时候这样用:new Promise()。使用Promise的时候,需要传一个函数:

new Promise(function(){})

上图中Promise{}是创建出的Promise的一个实例,实例的原型链指向Promise这个类的原型:

Promise{<pending>}.__proto__ === Promise.prototype  // => true
3,Promise有三个状态:
  • 1,pending 这叫初始状态(new Promise后的状态)
  • 2,fulfilled 这个叫成功状态(在executor函数中把resolve执行,就是告知Promise当前异步操作的结果是成功的)
  • 3,rejected 这个叫失败状态(在executor函数中把reject执行,就是告知Promise当前异步操作的结果是失败的)
// => 因为Promise是ES6中的一个内置类,我们用类的时候一般new,创建它的一个实例
new Promise()  // => 这样执行会报错  Promise resolver undefined is not a function, => new Promise()的时候必须得传一个函数,而这个函数就是我们的执行人(executor)执行者执行函数,用箭头函数也可以,只要是个函数就行
// => new Promise([executor]): 第一个执行函数必须传递
// => new Promise的时候就会把executor(执行函数)执行,创建Promise的一个实例
// => executor是promise这个类的一个回调函数(把一个函数作为值传给promise), Promise内部会把它执行
// => Promise不仅把它执行,而且还给executor(执行函数)传递两个参数(resolve, reject),这两个参数也是函数类型
// => resolve函数:它执行,代表Promise处理的异步事情是成功的,把Promise的状态改成fulfilled
// => reject函数:它执行,代表Promise处理的异步事情是失败的,把Promise的状态改成rejected
// => executor(执行函数)中放的就是当前要处理的异步操作事情
new Promise((resolve, reject) => {
  // => 这里一般存放的是我们即将要处理的异步任务,任务成功执行resolve, 任务失败执行reject,当然写同步的也可以,只是同步的就没必要用Promise来管理了,直接上面写完了下面用就可以了,没必要用promise来管理了
})
let Pro = new Promise((resolve, reject) => {
  setTimeout(() => {
    if (Math.random() < 0.5) {
      reject();
      return;
    }
    resolve();
  }, 1000);
});
/*
	1,执行executor执行函数,就意味着把函数中的异步操作开始执行,即设置一个定时器,1000ms后执行定时器方法
	然后等异步操作完成,异步操作完成后,通过执行resolve和reject修改new Promise实例(Pro)的状态.
	假设当前案例执行的是resolve,就会把状态改为fulfilled, reslove和reject在执行的时候我们可以给其传参,
	传递的参数值会修改Pro的value值[[PromiseResult]]
*/

我们使用Promise的重点不是把它的状态改成成功fulfilled和失败rejected,重点是让它成功的时候干什么,失败的时候干什么,那我们就用到Promise原型上的方法,Promise原型上一共有3个方法:then、catch、finally

4,Promise原型上的3个方法(then、catch、finally)
/*
	1, then: 设置成功或者失败后执行的方法(成功或者失败都可以设置,也可以只设置一个)
					Pro.then([success], [error])
					Pro.then([success])
					Pro.then(null, [error])
	2, catch: 设置失败后执行的方法
	3, finally: 设置不论成功还是失败都会执行的方法(这个方法一般不用)
*/
/**
	Promise.prototype.then()
	Promise.prototype.catch()
	Promise.prototype.finally()
*/
1,then: 用来写成功或者失败后执行的方法

因为原型上的方法,实例才能用

let Pro = new Promise((resolve, reject) => {
  setTimeout(() => {
    if (Math.random() < 0.5) {
      reject();
      return;
    }
    resolve();
  }, 1000);
});

Pro.then(res =>{
  // 状态为fulfilled成功后执行(res是[[PromiseValue]])
}, err =>{
  // 状态为 rejected 失败后执行
}) // => then方法里面可以写一个或者两个函数
// => 在异步操作完成之前,我们会基于then方法构建成功或者失败后要做的事情,相当于给实例构建一个事件池,当我们的状态更改,会立即通知事件池中的方法执行

new Promise创建一个实例Pro,初始状态[[PromiseStatus]]是pending, 初始值[[PromiseStatus]]是undefined

接下来执行executor函数,在executor函数中把这个setTimeout定时器异步操作执行,异步操作肯定需要拿时间等(异步:放到Event Queue中肯定需要等),此处不等,此时我们通过调用实例.then这个方法往事件池里面加了成功后要执行的方法A和失败后要执行的方法B,当异步操作成功,用户会根据自己的需求选择执行是resolve还是reject,当执行resolve的时候,把状态改为 fulfilled,当我们执行reject把状态改为rejected, 而且不管是执行resolve还是reject都可以传值,传的值都放在[[PromiseValue]]里了

let Pro = new Promise((resolve, reject) => {
  let ran = Math.random();
  setTimeout(() => {
    if (ran > 0.5) {
      reject(ran);
      return;
    }
    resolve(ran);
  }, 1000);
});
Pro.then(
  (res) => {
    console.log("成功: " + res);
  },
  (err) => {
    console.log("失败: " + err);
  }
);


// => 以上代码优化:
let Pro = new Promise((resolve, reject) => {
  let ran = Math.random();
  setTimeout(() => {
    ran > 0.5 ? reject(ran) : resolve(ran);
  }, 1000);
});
Pro.then(
  (res) => {
    console.log("成功: " + res);
  },
  (err) => {
    console.log("失败: " + err);
  }
);
2,catch

如果实例.then不想写失败的方法,那么我们可以使用catch来获取

let Pro = new Promise((resolve, reject) => {
  let ran = Math.random();
  setTimeout(() => {
    ran > 0.5 ? reject(ran) : resolve(ran);
  }, 1000);
});
Pro.then((res) => {
  console.log("成功: " + res);
});
Pro.catch((err) => {
  console.log("失败: " + err);
});
3,finally
let Pro = new Promise((resolve, reject) => {
  let ran = Math.random();
  setTimeout(() => {
    ran > 0.5 ? reject(ran) : resolve(ran);
  }, 1000);
});
Pro.then((res) => {
  console.log("成功: " + res);
});
Pro.catch((err) => {
  console.log("失败: " + err);
});
Pro.finally(() => {
  console.log("哈哈");
});

finally:不管执行的是resolve还是执行的是reject,都会把finally函数里的内容执行

扩展:JS中的try catch

// => 在JS中,当前行代码报错,会中断主线程的渲染(下面的代码将不再执行)
console.log(a);
let b = 10;
console.log(b);
// Uncaught ReferenceError: a is not defined
// => 如果上面的代码报错,不想影响到下面的代码执行,那么我们需要做异常捕获: try catch finally
try {
  console.log(a);
} catch (e) {
  // => 可以在catch里面捕获到错误信息
  console.log(e.message);
}
let b = 10;
console.log(b);

在真实项目中,我们不知道我们哪行代码会报错,那么我们可以把我们写的方法给try 起来,在catch里会把报错信息返回给服务器,服务器接收到会存起来,服务器会向开发人员发送嗷嗷报警的指令:当前用户操作有报错。

try里面不报错的话,catch就不会执行

5,Promise then链
let Pro = new Promise((resolve, reject) => {
  let ran = Math.random();
  setTimeout(() => {
    ran > 0.5 ? reject(ran) : resolve(ran);
  }, 1000);
});
Pro.then((res) => {
  console.log("成功: " + res);
});
// => 以上代码我们可以这样写:
new Promise((resolve, reject) => {
  let ran = Math.random();
  setTimeout(() => {
    ran > 0.5 ? reject(ran) : resolve(ran);
  }, 1000);
}).then(res => {
  
}, err => {});

执行.then/.catch/.finally返回的结果是一个全新的promise实例, 所以可以链式写下去。所以下一个then中哪个方法会被执行,由上一个then中某个方法执行的结果来决定

let Pro1 = new Promise((resolve, reject) => {
  let ran = Math.random();
  setTimeout(() => {
    ran > 0.5 ? reject(ran) : resolve(ran);
  }, 1000);
})
let Pro2 = Pro1.then(res => {
  
}, err => {})
Pro2 === Pro1 // => false
// => 待完善
new Promise((resolve, reject) => {
  resolve(100);
})
  .then(
    (res) => {
      console.log("res====: " + res);
    },
    (err) => {
      console.log("err====: " + err);
    }
  )
  .then(
    (A) => {
      console.log("A");
    },
    (B) => {
      console.log("B");
    }
  );

上一个then中某个方法的返回值会传递给下一个then的某个方法中

new Promise((resolve, reject) => {
  resolve(100);  // => 把第一个promise实例的[[PromiseValue]]值改为100,也就意味着在第一个then的两个方法里(成功方法或者失败方法)拿到的那个值就是这个[[PromiseValue]]值
})
  .then(
    (res) => {
      console.log("res====: " + res);
      return res * 100  // then中return相当于把当前这个新的promise实例中的[[PromiseValue]]值改为return返回的值(res*100), 传给第二个then
    },
    (err) => {
      console.log("err====: " + err);
    }
  )
  .then(
    (A) => {
      console.log("A: " + A);
    },
    (B) => {
      console.log("B: " + B);
    }
  );
// 以上代码的执行结果:res====: 100   A: 10000
// => 以上代码改一下
new Promise((resolve, reject) => {
    // resolve(100);
    reject(100);
  })
    .then(
      (res) => {
        console.log("res====: " + res);
        return res * 100;
      },
      (err) => {
        console.log("err====: " + err);
        return err / 10;
      }
    )
    .then(
      (A) => {
        console.log("A: " + A);
      },
      (B) => {
        console.log("B: " + B);
      }
    );
// 输出结果:err====: 100   A: 10
// => 我们可以看出走到第二个then的时候,都是走入了A函数,这里我们说了,第二个then事件池里到底是走A还是B方法取决于前一个then事件池里的方法有没有报错,即使前一个then事件池里的方法执行的是error方法,只要此error方法不报错,第二个then里就会走A方法

总结:第一次.then的时候,返回的是一个全新的Promise实例,所以说可以继续.then下去形成我们链式写法,但是再继续第二次.then里面放那两个方法到底哪一个执行取决于上面第一次.then的这个实例的成功还是失败的状态,不管第一个.then里的res方法执行还是err方法执行,这两个方法必然只会执行一个方法,执行的这一个方法如果没有报错,那就是成功,如果有报错,那就是失败

catch、finally和then一样,都是返回一个新的promise实例

现实项目中:

new Promise((resolve, reject) => {
  resolve(100);
  // reject(100);
})
  .then((A) => {
    console.log("A: " + A);
  })
  .catch((B) => {
    console.log("B: " + B);
  });
// => 现实项目中我们一般都是这么用
// => 以上代码 执行resolve方法的时候,输出的结果是A: 100.   执行reject方法的时候,输出的结果是B: 100

如果当前Promise实例的状态确定后,都会到对应的then里找方法, 如果then中没有对应的这个方法,则会向下一个then中查找(向下顺延)

new Promise((resolve, reject) => {
  resolve(100);
  // reject(100);
})
  .then((A) => {
    console.log("A: " + AAAA);  // => 找不到AAAA这个变量,执行报错,让.then创建的promise实例变为失败状态,并且把报错的原因修改为promise的value值
  })
  .catch((B) => {
    console.log("B: " + B);
  });
// => B: ReferenceError: AAAA is not defined
new Promise((resolve, reject) => {
  resolve(100);
  // reject(100);
})
  .then((A) => {
    console.log("A: " + AAAA);
  })
  .catch((B) => {
    console.log("B: " + B);
  	return '@'
  })
  .then((C) => {
    console.log("C:  " + C);
  });
// => B: ReferenceError: AAAA is not defined    C:  @
6,Promise.all()

Promise.all():执行all返回的也是一个新的Promise实例, 所有的promise实例返回状态都为成功才会成功。如果输入的任何 Promise 被拒绝,则返回的 Promise 将被拒绝,并带有第一个被拒绝的原因

// => Promise.all([promise1, promise2, promise3....]): all中存放的是多个promise实例,每一个实例管理着一个异步操作,执行all方法返回的结果是一个新的Promise实例,假设这个Promise实例叫PROA
/* 当所有的Promise实例的状态都为fulfilled的时候(成功的时候),让PROA的状态也变为fulfilled,并且把所有Promise成功获取的结果,存储为一个数组(顺序和最开始编写的顺序一致):result = [result1, result2, result3...]
	让PROA这个数组的value值等于这个数组
	只有都成功PROA状态才是fulfilled,才会通知then中第一个方法执行,只要有一个失败PROA状态就是rejected,就会通知then中第二个方法或者catch中的方法执行
*/
function ajax1() {
  return new Promise((resolve) => {
    $.ajax({
      url: "/api1",
      success: resolve,
    });
  });
}
function ajax2() {
  return new Promise((resolve) => {
    $.ajax({
      url: "/api2",
      success: resolve,
    });
  });
}
function ajax3() {
  return new Promise((resolve) => {
    $.ajax({
      url: "/api3",
      success: resolve,
    });
  });
}
Promise.all([ajax1(), ajax2(), ajax3()])
  .then((res) => {
  /** 
  => res: [result1, result2, result3...]
  => res是一个数组,数组里是ajax1得到的结果,ajax2得到的结果,ajax3得到的结果
  **/
  console.log(res);
})
  .catch((err) => {
  // => 只要有一个出错了就会执行它了
  console.log(err);
});
7,Promise.race()

race和all正好相反,只要有一个成功就会立即把PROA的装套改为fulfilled,把then中的方法执行,获取的结果是看哪一个promise实例的状态最先处理完(成功或者失败),以最先处理完的为主

8,Promise.allSettled()

Promise.allSettled() 它接收一个由多个 Promise 对象组成的可迭代对象,并在所有 Promise 对象都已解决或拒绝后返回一个带有描述每个 Promise 结果的对象数组

Promise.allSettled([
  Promise.resolve(33),
  new Promise((resolve) => setTimeout(() => resolve(66), 0)),
  99,
  Promise.reject(new Error("一个错误")),
]).then((values) => console.log(values));
// [
//   { status: 'fulfilled', value: 33 },
//   { status: 'fulfilled', value: 66 },
//   { status: 'fulfilled', value: 99 },
//   { status: 'rejected', reason: Error: 一个错误 }
// ]
9,Promise.any()

它接收一个由多个 Promise 对象组成的可迭代对象,当输入的任何一个 Promise 成功时,这个返回的 Promise 将会兑现,并返回第一个兑现的值

const promise1 = Promise.reject(0);
const promise2 = new Promise((resolve) => setTimeout(resolve, 100, 'quick'));
const promise3 = new Promise((resolve) => setTimeout(resolve, 500, 'slow'));
const promises = [promise1, promise2, promise3];
Promise.any(promises).then((value) => console.log(value));

// Expected output: "quick"
10,Promise.withResolvers()

Promise.withResolvers() 静态方法返回一个对象,其包含一个新的Promise对象、reslove函数、reject函数,他的作用是把 Promise实例、resolve、reject 解构出来供我们使用

// => 之前我们想要在外部使用resolve 和 reject方法需要如下操作
let a, b;
new Promise((resolve, reject) => {
  a = resolve;
  b = reject;
});

// => 现在我们可以通过静态方法更方便的获取resolve,reject
const { promise: p, resolve: a, reject: b } = Promise.withResolvers();
p.then((res) => {
  console.log(res);
});
setTimeout(() => {
  a(123);
}, 1000);
// 1s后输出123

6,用户从浏览器输入url到页面最后呈现,经历了哪些过程?

这是一道常规的面试题,考的是基本网络原理和浏览器加载css、js的过程。

当用户输入url按下enter键到页面中看到整个内容,中间经历了7个步骤:

1,url地址解析

url解析的主要原因是它得从一个url或者uri中解析出那几个部分:协议、域名、端口号、请求资源路径名称、问号传参和HASH值,把每一部分解析到,并且如果涉及到加密的地方,需要编码的地方可以通过encodeURI或者encodeURIComponent方式来进行编码加密,而编码默认都是基于encodeURI来编码的

2,DNS域名解析

然后到服务器之前,到DNS服务器上根据域名找到服务器对应的外网ip,这叫做DNS域名解析,它里面有个优化手段叫DNS缓存或DNS预获取(dns-prefetch)

3,发起TCP连接

通过外网ip跟服务器之间建立一个tcp的连接通道,这里需要三次握手

3次握手都干些啥事呢?通俗点讲,第一件事浏览器告诉服务器端我要发请求了,你准备吧,服务器告诉客户端我准备好了,你发吧,浏览器:我准备发了,你准备接收吧,浏览器首先传给服务器一个SYN的标识结果是1,并传了一个seq的标识,传一个标识符比如事A,传给服务器了,服务器拿到后,返回一个SYN是1返回seq可能返回的是B,但是多返回一个叫ACK的东西,这里面包含客户端传来的2个标识相加的结果,返回给客户端,客户端拿到后除了SYN和seq,再把服务器返回的ACK+1一并返回

4,发送http请求

握手成功后, 浏览器就可以向服务器发送http请求了,发送HTTP请求的过程就是构建HTTP请求报文并通过TCP协议中发送到服务器指定端口(HTTP协议80/8080, HTTPS协议443)。HTTP请求报文是由三部分组成: 请求行、请求头、请求主体

请求报文:所有经过传输协议,客户端传递给服务器的内容,都被称为请求报文

1,请求行
// => 请求格式:
Method  RequestURL  HTTPVersion CRLF
例如:GET index.html HTTP/1.1
常用的请求方法有:get、post、put、delete、options、head
2,请求头
// => 请求报头
请求报头允许客户端向服务器传递请求的附加信息
常见的请求头有: 
	1,Accept:指定客户端接受哪些类型的信息,其值为image/gif、text/html
  2,Accept-Encoding:可接收的内容编码,其值为gzip-压缩类型、identify-默认
	3,Accept-Language:指定一种自然语言,其值为zh-cn
	4,ContentType:body编码方式
  5,Authorization:证明客户端有权访问某个资源
  6,Cookie:和Authorization一样
  7,UserAgent:用户代理,其值为浏览器及版本、浏览器语言、浏览器渲染引擎、浏览器插件、CPU类型、操作系统及版本
  8,Connection:值为 Keepalive 用于告诉客户端本次 HTTP 请求结束之后并不需要关闭 TCP 连接, 这样可以使下次 HTTP 请求使用相同的 TCP 通道, 节省 TCP 连接建立的时间
3,请求主体

当使用POST, PUT等方法时,通常需要客户端向服务器传递数据。这些数据就储存在请求主体中。在请求头中有一些与请求正文相关的信息,例如: 现在的Web应用通常采用Rest架构,请求的数据格式一般为json。这时就需要设置Content-Type: application/json

5,服务器响应

后端接收到TCP报文后,会对TCP连接进行处理,对HTTP协议进行解析,并按照报文格式进一步封装成HTTP Request对象,供上层使用。这部分工作一般由web服务器去进行,常用的web服务器有Tomcat、IIS、Netty等

HTTP响应报文也有3部分组成:HTTP状态码、响应头、响应主体

HTTP状态码

http状态码是1~5开头的,三位数字。

  • 1开头的都代表处理中
  • 2开头的:都代表成功
    • 200: 成功
    • 201: 成功,一般应用于告诉服务器创建一个新文件,最后服务器创建成功后返回的状态码
    • 204: 成功,对于某些请求(post或者delete),服务器不想处理,可以返回空内容,并且用204状态码告知
  • 3开头的:成功了但是需要周转一下
    • 301: 永久重定向(永久转移),什么时候能用到它呢?就是我像A服务器发个请求,它告诉我我不处理了,我给你换个地,然后把我这个请求交给另外一台服务器处理了。一般应用于服务器迁移/域名迁移
    • 302: 临时重定向/临时转移,很早以前用302来做,但是现在主要用307来处理,因为307的意思就是临时重定向。临时重定向主要用于服务器负载均衡
    • 304: 设置HTTP的协商缓存
  • 4开头的:错误,一般是客户端的错误
    • 400: 错误的请求,一般应用于传递给服务器的参数错误
    • 401: 无权限访问
    • 403: 当前服务器不允许我们设置请求头
    • 404: 请求地址错误
  • 5开头的:错误,一般是服务器的错误
    • 500: 未知服务器错误:不知道服务器为啥错了,可能宕机了,可能某个程序卡顿导致服务器处理不过来了
    • 503: 服务器超负荷,服务器处理不过来了

HTTP报文:请求报文 + 响应报文

6,客户端进行页面渲染

服务器返回给浏览器的文本信息, 通常是html、css、js、图片、视频等文件,浏览器渲染页面的步骤:

  • 1,解析html,生成DOM树,解析CSS,生成CSSOM树
  • 2,将DOM树和CSSOM树结合,生成渲染树(Render Tree)
  • 3,Layout(回流):根据生成的渲染树,计算它们在设备视口(viewport)内的确切位置和大小,这个阶段是回流
  • 4,Painting(重绘):根据渲染树以及回流得到的几个信息,得到节点的绝对像素
  • 5,Display:将像素发送给GPU,将各个节点绘制到屏幕上

浏览器是多线程的,但是渲染代码的只是单线程的。浏览器是多线程的,它里面有好多人干活,它只是派了一个人过来自上而下一行一行渲染代码。

当遇到link的时候,浏览器会分配一个新的线程去加载这个资源文件,但是渲染页面的那个主线程会继续向下渲染。

遇到script标签也是向link一样处理。

script标签是同步还是异步:默认是同步

页面中遇到link/img/audio/video等是异步去加载资源信息(浏览器分配一个新的线程去加载,主线程继续向下渲染页面),如果遇到script/@import(引入css的),则让主线程去加载资源信息(同步),加载完成信息后,再去继续渲染页面。

7,断开tcp连接,4次挥手
  • 第1次挥手:由浏览器发起,发给服务器,我请求报文发送完了,你准备关闭吧
  • 第2次挥手:由服务器发起,告诉浏览器,我接收完请求报文了,我准备关闭,你也准备吧
  • 第3次挥手:由服务器发起,告诉浏览器,我响应报文发送完毕,你准备关闭吧
  • 第4次挥手:由浏览器发起,告诉服务器,我响应报文接受完毕,我准备关闭,你也准备吧

4次握手一定建立在传输信息之前,因为我通道没有建立起来,我不能传递信息。在传输信息之前,我握了3次手建立了连接

4次挥手是发生在信息传输的过程中的:当客户端把信息传给服务器就挥了第1次手;当服务器接收到请求后就挥了第2次手,告诉他我接收到了;当服务器把东西返回给客户端的时候就挥了第3次手;当客户端拿了服务器返回的东西就挥了第4次手

握手和挥手的过程也耗时间耗资源,最好能保证当我跟你建立连接握完手不断开连接一直保持连接,下次再发请求,就不用握手了,直接把东西给你就行了,这就叫tcp协议的长连接:一直保持着客户端和服务器端连接不中断。这东西怎么做呢?在我们的报文中有一个Connection: Keep-Alive保持tcp不中断。这也是性能优化之一。这个需要我们这边设置服务器那边也要设置


有个问题,客户端一直和服务器端保持着连接,对服务器的压力肯定会有一定的影响的,我们可以设置一个建立连接的时间点,在某个时间内让它过期,或者在某个时间阶段里,让服务器主动把这个连接断开。

7,DOM的重绘(Repaint)和重排(回流)(Reflow)

1,重绘
  • 重绘: 元素样式的改变(但宽高、大小、位置等不变)
    • 比如我只是把当前元素的背景颜色变了
    • Outline、visibility、color、background-color等
2,重排
  • 重排:元素的大小或者位置发生变化(当页面布局和几何信息发生变化的时候),触发了重新布局,导致渲染树重新计算布局和渲染
    • 如添加或者删除可见的DOM元素
    • 元素的位置发生变化
    • 元素的尺寸发生变化
    • 内容发生变化(比如文本变化或者图片被另一个不同尺寸的图片所替代)
    • 页面一开始渲染的时候,这个无法避免,因为回流是根据视口大小来计算元素的位置和大小的,所以浏览器的窗口尺寸变化会引发回流
    • 回流就是结构得重新算这就是回流

注意:重排一定会触发重绘,重绘不一定触发重排

3,避免DOM的回流的方案
1,放弃传统操作DOM的时代,基于vue/react开始数据影响视图模式

Mvvm、mvc、virtual dom、dom diff

2,DOM操作的分离读写(现代的浏览器都有渲染队列的机制)
offsetTop、offsetLeft、offsetWidth、offsetHeight、clientTop、clientLeft、clientWidth、clientHeight、scrollTop、scrollLeft、scrollWidth、scrollHeight、getComputedStyle、currentStyle....这些都会刷新渲染队列
<div id="box"></div>
box.style.width = "100px";
box.style.height = "100px";
box.style.background = "red";   // => 重绘制
box.style.margin = "20px auto";
// => 以上代码会引发几次回流(重排)?
// => 1次
// => 因为现代版浏览器都有“渲染队列”机制:发现某一行要修改元素的样式,不立即渲染,而是看看下一行,如果下一行也会改变样式,则把修改样式的操作放到“渲染队列中”.....一直到不再是修改样式的操作后,整体渲染一次,引起一次回流

读写分离:

box.style.width = "100px";  // => 写
console.log(box.offsetWidth)   // => 读
box.style.height = "100px";  // => 写
console.log(box.offsetHeight)   // => 读
box.style.background = "red";  // => 写
box.style.margin = "20px auto";  // => 写
// => 以上读的时候(console.log(box.offsetWidth)),会引发一次回流,所以以上代码会引发3次回流。所以要读写分离

box.style.width = "100px";  // => 写
box.style.height = "100px";  // => 写
box.style.background = "red";  // => 写
box.style.margin = "20px auto";  // => 写
console.log(box.offsetWidth)   // => 读
console.log(box.offsetHeight)   // => 读
// => 把读和写分开来做
3,样式集中改变
.box {
  width: 100px;
  height: 100px;
  background-color: aqua;
  margin: 20px auto;
}
<div id="boxId"></div>

// 方式一:
div.className = 'box'

// => 方式二:
div.style.cssText = 'width: 20px; height: 20px'
4,缓存布局信息(它的原理是读写分离)
div.style.left = div.offsetLeft + 1 + 'px'
div.style.top = div.offsetTop + 1 + 'px'

// => 改为
var curLeft = div.offsetLeft
var curTop = div.offsetTop
div.style.left = curLeft + 1 + 'px'
div.style.top = curTop + 1 + 'px'
5,元素批量修改

1, 文档碎片:createDocumentFragment

// => 假设我们想给box动态加10个 span
<div id="box"></div>;

<script>
  for (let i = 0; i < 10; i++) {
    let span = document.createElement("span");
    span.innerHTML = i;
    box.appendChild(span);
  }
  // => 以上方法引发10次回流

  // => 方式1: 使用文档碎片
  let frg = document.createDocumentFragment();
  // 文档碎片:存储文档的容器
  for (let i = 0; i < 10; i++) {
    let span = document.createElement("span");
    span.innerHTML = i;
    frg.appendChild(span);
  }
  box.appendChild(frg);
  frg = null;
  // => 引发一次回流

  // => 方式2: 使用模版字符串拼接
  let str = ``;
  for (let i = 0; i < 10; i++) {
    str += `<span>${i}</span>`;
  }
  box.innerHTML = str;
  // => 引发一次回流
</script>
6,动画效果应用到position属性为absolute或者fixed的元素上,因为它们脱离文档流,不会引发其他元素的回流
7,CSS3硬件加速(GPU加速)

比起考虑如何减少回流重绘,我们更期望的是,根本不要回流重绘:transform/opacity/filters…这些属性会触发硬件加速,不会引发回流和重绘

可能会引发的坑:过多使用会占用大量内存,性能消耗严重,有时候会导致字体模糊等

8,牺牲平滑度换取速度

每次1像素移动一个动画,但是如果此动画使用了100%的CPU,动画就会看上去是跳动的,因为浏览器正在与更新回流做斗争,每次移动3像素可能看起来平滑度低了,但它不会导致CPU在较慢的机器中抖动

9,避免table布局和使用css的javascript表达式

【END:如果对你有所帮助,非常期待你关注一下,便于及时收到更新的内容~】

下一篇文章内容预告:

1,理解3次握手4次挥手
2,函数柯里化
3,前端性能优化
4,在什么情况下能让这个成立:a1&&a2&&a==3的时候console.log(1)
5,排序(冒泡、插入、快速)
6,弄懂为什么0.1+0.2不等于0.3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值