Giter Site home page Giter Site logo

interview-question's Introduction

Hey 👋🏽, I'm Yanlele

About me

  • Web Development Engineer
  • Currently working with JavaScript and TypeScript
  • Continuous learning: Desktop, mobile, fronted, backend, devOps, games, designer

Blogs 🌱


Languages and Tools...

Here are some technologies I use at work

Badge Badge Badge Badge Badge Badge Badge Badge Badge Badge Badge Badge Badge Badge Badge


Other Languages I know

Badge Badge Badge


My contributions to open-source:

interview-question's People

Contributors

yanlele avatar

Stargazers

 avatar  avatar

Watchers

 avatar

interview-question's Issues

跨域通信的常见方式有哪些?

常见方式

JSONP
JSONP是通过动态创建script标签的方式,利用script标签可以跨域请求资源的特性来实现的,本质是利用了script标签没有跨域限制的特性,可以在请求的url后加一个callback参数,后端接收到请求后,将需要传递的数据作为参数传递到callback函数中,前端定义该函数来接收数据,从而实现跨域通信。
#27

CORS
CORS是一种现代浏览器支持的跨域解决方案,CORS全称为跨域资源共享(Cross-Origin Resource Sharing),其本质是在服务端设置允许跨域访问的响应头,浏览器通过判断响应头中是否允许跨域访问来决定是否允许跨域访问。
#28

postMessage
postMessage是HTML5引入的一种新的跨域通信方式,主要是用于在不同窗口之间进行通信,包括不同域名、协议、端口等情况,通过调用window.postMessage()方法,在两个窗口之间发送消息,接收方通过监听message事件来接收消息,从而实现跨域通信。
#29

WebSocket
WebSocket是一种新的网络协议,可以实现客户端和服务器之间的实时双向通信,同时也可以跨域通信,WebSocket协议建立在TCP协议之上,通过HTTP协议发起握手请求,握手成功后,客户端和服务器就可以通过WebSocket协议进行实时通信了。
#30

代理转发
代理转发是一种常用的跨域通信方式,主要是通过在同一域名下设置代理服务器,在代理服务器上实现跨域访问,再将结果返回给前端页面,从而实现跨域通信。

ES6 Generator 了解多少?

Generator 基本概念

ES6中的 Generator(生成器)是一种特殊类型的函数,它可以被暂停和恢复。这意味着在调用Generator函数时,它不会立即执行,而是返回一个可暂停执行的Generator对象。在需要的时候,可以通过调用.next()方法来恢复函数的执行。这使得我们能够编写更具表现力和灵活性的代码。

Generator函数使用特殊的语法:在函数关键字后面添加一个星号(*)。Generator函数中可以使用一个新的关键字yield,用于将函数的执行暂停,并且可以将一个值返回给调用者。

以下是一个简单的 Generator 函数的例子:

function* generateSequence() {
  yield 1;
  yield 2;
  yield 3;
}

let generator = generateSequence();

console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log(generator.next().value); // 3

在上面的例子中,generateSequence()是一个Generator函数,它定义了一个简单的数列生成器。在函数中,使用了yield关键字,以便能够暂停函数执行。最后,我们通过调用generator.next()方法来恢复函数的执行,并逐步返回生成器中的每一个值。

Generator函数也可以接收参数,并且可以在每次迭代时使用不同的参数值。这使得它们能够以更灵活的方式生成数据。

以下是一个带参数的Generator函数的例子:

function* generateSequence(start, end) {
  for (let i = start; i <= end; i++) {
    yield i;
  }
}

let generator = generateSequence(1, 5);

console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log(generator.next().value); // 3
console.log(generator.next().value); // 4
console.log(generator.next().value); // 5

Generator是一种非常有用的工具,它能够帮助我们编写更灵活和表达力强的代码。它们在异步编程、迭代器和生成器等场景中得到了广泛的应用。

与 async/await 有啥关系?

Generator 和 async/await 都是 ES6 中引入的异步编程解决方案,它们本质上都是利用了 JavaScript 中的协程(coroutine)。

Generator 和 async/await 都是 JavaScript 中用于异步编程的方式,它们的本质相同,都是利用了生成器函数的特性来实现异步操作。

在 ES5 中,JavaScript 使用回调函数实现异步编程,但是这样会导致回调嵌套过深,代码可读性差、难以维护。Generator 和 async/await 的出现解决了这个问题,它们让异步编程更加像同步编程,使代码可读性和可维护性得到了大幅提升。

Generator 可以使用 yield 语句来暂停函数执行,并返回一个 Generator 对象,通过这个对象可以控制函数的继续执行和结束。而 async/await 则是基于 Promise 实现的语法糖,可以使异步代码看起来像同步代码,代码结构更加清晰明了。

在使用上,Generator 和 async/await 都需要通过一些特定的语法来实现异步操作,不同的是 async/await 通过 await 关键字等待 Promise 对象的解决,而 Generator 则是通过 yield 关键字暂停函数执行,并返回一个 Generator 对象,通过 next 方法控制函数的继续执行和结束。另外,async/await 可以更好地处理 Promise 的错误,而 Generator 需要使用 try/catch 语句来捕获错误。

Generator 和 async/await 可以互相转换,这意味着我们可以使用 Generator 来实现 async/await 的功能,也可以使用 async/await 来调用 Generator 函数。

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}

async function test() {
  for await (const x of gen()) {
    console.log(x);
  }
}

test(); // 输出 1, 2, 3

在上面的代码中,for await...of 循环语句可以遍历 Generator 函数生成的迭代器,从而实现异步迭代。注意在调用 for await...of 时需要使用 yield* 关键字来进行委托。

Generator 函数使用 await 调用示例:

function* generator() {
  const result1 = yield asyncTask1();
  const result2 = yield asyncTask2(result1);
  return result2;
}

async function runGenerator() {
  const gen = generator();
  const result1 = await gen.next().value;
  const result2 = await gen.next(result1).value;
  const finalResult = await gen.next(result2).value;
  console.log(finalResult);
}

runGenerator();

es6 数据结构 Set 了解多少?

Set

基本概念

Set 对象允许你存储任何类型的唯一值,无论是原始值或者是对象引用。

Set对象是值的集合,你可以按照插入的顺序迭代它的元素。Set 中的元素只会出现一次,即 Set 中的元素是唯一的。

另外,NaN 和 undefined 都可以被存储在 Set 中,NaN 之间被视为相同的值(NaN 被认为是相同的,尽管 NaN !== NaN)。

有哪些属性和方法

操作方法:
add(value):添加某个值,返回 Set 结构本身。
delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
has(value):返回一个布尔值,表示该值是否为Set的成员。
clear():清除所有成员,没有返回值。

遍历方法:
keys():返回键名的遍历器
values():返回键值的遍历器
entries():返回键值对的遍历器
forEach():使用回调函数遍历每个成员
Set.prototype[@@iterator](): 返回一个新的迭代器对象,该对象包含 Set 对象中的按插入顺序排列的所有元素的值。

Set.prototype[@@iterator]() 较为特殊, 细说一下:
@@iterator 属性的初始值和 values 属性的初始值是同一个函数。

const mySet = new Set();
mySet.add('0');
mySet.add(1);
mySet.add({});

const setIter = mySet[Symbol.iterator]();

console.log(setIter.next().value); // "0"
console.log(setIter.next().value); // 1
console.log(setIter.next().value); // Object

一些实用场景

// 判断是会否属于: B 是否属于 A
function isSuperset(set, subset) {
    for (let elem of subset) {
        if (!set.has(elem)) {
            return false;
        }
    }
    return true;
}

// 合集
function union(setA, setB) {
    let _union = new Set(setA);
    for (let elem of setB) {
        _union.add(elem);
    }
    return _union;
}

// 交集
function intersection(setA, setB) {
    let _intersection = new Set();
    for (let elem of setB) {
        if (setA.has(elem)) {
            _intersection.add(elem);
        }
    }
    return _intersection;
}

// 对称差分
function symmetricDifference(setA, setB) {
    let _difference = new Set(setA);
    for (let elem of setB) {
        if (_difference.has(elem)) {
            _difference.delete(elem);
        } else {
            _difference.add(elem);
        }
    }
    return _difference;
}

// 属于 A 但是不属于 B
function difference(setA, setB) {
    let _difference = new Set(setA);
    for (let elem of setB) {
        _difference.delete(elem);
    }
    return _difference;
}

//Examples
let setA = new Set([1, 2, 3, 4]),
    setB = new Set([2, 3]),
    setC = new Set([3, 4, 5, 6]);

isSuperset(setA, setB);          // => true
union(setA, setC);               // => Set [1, 2, 3, 4, 5, 6]
intersection(setA, setC);        // => Set [3, 4]
symmetricDifference(setA, setC); // => Set [1, 2, 5, 6]
difference(setA, setC);          // => Set [1, 2]

WeakSet

基本概念

WeakSet 对象允许你将弱保持对象存储在一个集合中。

WeakSet 对象是一些对象值的集合。且其与 Set 类似,WeakSet 中的每个对象值都只能出现一次。在 WeakSet 的集合中,所有对象都是唯一的。

它和 Set 对象的主要区别有:

  • WeakSet 只能是对象的集合,而不能像 Set 那样,可以是任何类型的任意值。
  • WeakSet 持弱引用:集合中对象的引用为弱引用。如果没有其它的对 WeakSet 中对象的引用,那么这些对象会被当成垃圾回收掉。

这也意味着 WeakSet 中没有存储当前对象的列表。正因为这样,WeakSet 是不可枚举的

实例方法

  • WeakSet.prototype.add(value): 将 value 添加到 WeakSet 对象最后一个元素的后面。

  • WeakSet.prototype.delete(value): 从 WeakSet 中移除 value。此后调用 WeakSet.prototype.has(value) 将返回 false。

  • WeakSet.prototype.has(value): 返回一个布尔值,表示 value 是否存在于 WeakSet 对象中。

使用场景 - 检测循环引用

// 对 传入的 subject 对象 内部存储的所有内容执行回调
function execRecursively(fn, subject, _refs = new WeakSet()) {
  // 避免无限递归
  if (_refs.has(subject)) {
    return;
  }

  fn(subject);
  if (typeof subject === "object") {
    _refs.add(subject);
    for (const key in subject) {
      execRecursively(fn, subject[key], _refs);
    }
  }
}

const foo = {
  foo: "Foo",
  bar: {
    bar: "Bar",
  },
};

foo.bar.baz = foo; // 循环引用!
execRecursively((obj) => console.log(obj), foo);

Map 和 Object 有哪些主要的区别?

不过 Map 和 Object 有一些重要的区别,在下列情况中使用 Map 会是更好的选择:

Map Object
意外的键 Map 默认情况不包含任何键。只包含显式插入的键。

一个 Object 有一个原型,原型链上的键名有可能和你自己在对象上的设置的键名产生冲突。

备注:虽然可以用 Object.create(null) 来创建一个没有原型的对象,但是这种用法不太常见。

键的类型 一个 Map 的键可以是任意值,包括函数、对象或任意基本类型。 一个 Object 的键必须是一个 String 或是 Symbol
键的顺序

Map 中的键是有序的。因此,当迭代的时候,一个 Map 对象以插入的顺序返回键值。

虽然 Object 的键目前是有序的,但并不总是这样,而且这个顺序是复杂的。因此,最好不要依赖属性的顺序。

自 ECMAScript 2015 规范以来,对象的属性被定义为是有序的;ECMAScript 2020 则额外定义了继承属性的顺序。参见 OrdinaryOwnPropertyKeysEnumerateObjectProperties 抽象规范说明。但是,请注意没有可以迭代对象所有属性的机制,每一种机制只包含了属性的不同子集。(for-in 仅包含了以字符串为键的属性;Object.keys 仅包含了对象自身的、可枚举的、以字符串为键的属性;Object.getOwnPropertyNames 包含了所有以字符串为键的属性,即使是不可枚举的;Object.getOwnPropertySymbols 与前者类似,但其包含的是以 Symbol 为键的属性,等等。)

Size Map 的键值对个数可以轻易地通过 size 属性获取。 Object 的键值对个数只能手动计算。
迭代 Map可迭代的 的,所以可以直接被迭代。

Object 没有实现 迭代协议,所以使用 JavaSctipt 的 for...of 表达式并不能直接迭代对象。

备注:

性能

在频繁增删键值对的场景下表现更好。

在频繁添加和删除键值对的场景下未作出优化。

序列化和解析

没有元素的序列化和解析的支持。

(但是你可以使用携带 replacer 参数的 JSON.stringify() 创建一个自己的对 Map 的序列化和解析支持。参见 Stack Overflow 上的提问:How do you JSON.stringify an ES6 Map?

原生的由 Object 到 JSON 的序列化支持,使用 JSON.stringify()

原生的由 JSON 到 Object 的解析支持,使用 JSON.parse()

如何检测对象是否循环引用?

检测循环引用

例如下面的场景, 已经出现循环引用了, 如何检测?

const foo = {
  foo: "Foo",
  bar: {
    bar: "Bar",
  },
};

foo.bar.baz = foo; // 循环引用!

解答:使用 WeakSet 特性解决;

// 对 传入的 subject 对象 内部存储的所有内容执行回调
function execRecursively(fn, subject, _refs = new WeakSet()) {
  // 避免无限递归
  if (_refs.has(subject)) {
    return;
  }

  fn(subject);
  if (typeof subject === "object") {
    _refs.add(subject);
    for (const key in subject) {
      execRecursively(fn, subject[key], _refs);
    }
  }
}

const foo = {
  foo: "Foo",
  bar: {
    bar: "Bar",
  },
};

foo.bar.baz = foo; // 循环引用!
execRecursively((obj) => console.log(obj), foo);

参考:#35

js 对象可以使用 for...of 迭代吗?

JavaScript 对象本身并不能直接使用 for...of 迭代,因为它并不是一个可迭代对象(iterable)。

但是,如果我们想要遍历对象的属性,可以使用 for...in 循环,例如:

const obj = {
  name: 'John',
  age: 30,
  city: 'New York'
};

for (let prop in obj) {
  console.log(prop + ': ' + obj[prop]);
}

// 这段代码可以输出:
name: John
age: 30
city: New York

需要注意的是,for...in 循环会遍历对象自身的所有可枚举属性(不包括原型链上的属性),包括非数字键和继承的属性。如果只想遍历对象自身的属性,可以使用 hasOwnProperty() 方法进行判断,例如:

const obj = {
  name: 'John',
  age: 30,
  city: 'New York'
};

for (let prop in obj) {
  if (obj.hasOwnProperty(prop)) {
    console.log(prop + ': ' + obj[prop]);
  }
}

这段代码和上面的代码功能是一样的,但是多了一个 hasOwnProperty() 判断,可以确保只输出对象自身的属性。

手写 async 函数?

async/await 的本质

async/await 是 ECMAScript 2017(ES8)中引入的一个语言特性,用于处理异步编程。async/await 实际上是对 Promise 的封装,通过让开发者以同步的方式编写异步代码,使得代码更加易读和易于维护。

async/await 是一种更加高级的异步编程方式,它使用了 Promise 作为底层实现,可以更好地处理异步编程中的错误和异常,避免了回调地狱和代码可读性差的问题。

手写 async/await 实现

async/await 的实现可以通过封装 Promise 和 Generator 函数来实现,下面是一个简单的手写实现示例:

function delay(ms) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, ms);
  });
}

function* generator() {
  console.log("start");
  yield delay(1000);
  console.log("after 1 second");
  yield delay(2000);
  console.log("after 2 more seconds");
}

function async(generatorFunc) {
  const iterator = generatorFunc();

  function handle(iteratorResult) {
    if (iteratorResult.done) {
      return Promise.resolve(iteratorResult.value);
    }

    return Promise.resolve(iteratorResult.value).then((res) => {
      return handle(iterator.next(res));
    });
  }

  return handle(iterator.next());
}

async(function () {
  return generator();
}).then(() => {
  console.log("all done");
});

ajax如何获取下载进度?

ajax如何获取下载进度?

要获取下载进度,可以使用 XMLHttpRequest 对象提供的 onprogress 事件。

使用 onprogress 事件,可以获取文件的下载进度信息,可以通过 loaded 和 total 属性获取当前已经下载的字节数和文件的总字节数,从而计算出当前的下载进度。

下面是一个使用 onprogress 事件获取文件下载进度的示例代码:

const xhr = new XMLHttpRequest();
xhr.open('GET', 'file.url', true);
xhr.responseType = 'blob';
xhr.onprogress = function (event) {
  if (event.lengthComputable) {
    const percentComplete = (event.loaded / event.total) * 100;
    console.log(`Downloaded ${percentComplete}%`);
  }
};
xhr.onload = function (event) {
  // 文件下载完成
  const blob = xhr.response;
};
xhr.send();

在上面的代码中,通过将 XMLHttpRequest 对象的 responseType 设置为 blob,来请求一个文件资源,然后监听 onprogress 事件,计算出当前的下载进度,并在控制台输出,最后在 onload 事件中获取到下载的文件内容。

解释一下 原型、构造函、实例、原型链 之间的关系?

创建对象有哪几种方式?

// 面向字面量
var o1={name:'01'};
var o11=new Object({name:'o11'});

// 使用显示的构造函数:
var M=function(){this.name='02'};
var o2=new M();

// 通过Object.create()创建
var P={name:'o3'};
var o3=Object.create(P)

解释一下 原型、构造函、实例、原型链 之间的关系?

01_01

1、基础

构造函数可以通过new来生成一个实例、构造函数也是函数;
函数都有一个prototype属性,这个就是原型对象;
原型对象可以通过构造器constructor来指向它的构造函数;
实例的__proto__属性,指向的是其构造函数的原型对象;

原型链:从一个实例对象,向上找构造这个实例相关联的对象,相关联的对象又向上找,找到创造它的一个实例对象,
一直到Object.prototype截止。原型链是通过prototype和__proto__向上找的。构造函数通过prototype创建了很多方法,
被其所有实例所公用,存放在原型对象上;

例子:

var M=function(name){this.name=name};
var o3=new M('o3');

当我们需要扩展实例的时候,我们可以对构造函数添加方法,但是这样会创建每一个实例都拷贝一份它自己的添加的方法,
占用内存,而且也没有必要,这个时候就可以新添加的方法写进原型里面去,添加到原型链中去,
在实例的原型链中我们可以在原型对象上找到添加的方法;

var M=function(name){this.name=name};
var o3=new M('o3');
M.prototype.say=function(){
Console.log('say hi');
};
var o5=new M('o5');

通过这种方式o3和o5都有say方法;原型链的优势是原型对象的方法是被所有实例共有的;

当访问一个实例方法的时候,首先在实例本身找这个方法,如果没有找到就会到其构建函数的原型对象去找,如果还是没有找到,
那么它会继续通过原型链在原型对象的更上一级查找,一直到object.prototype;

一定要记住只有函数才有proptotype,对象是没有的;

只有实例对象又__proto__ , 因为函数也是对象,所以函数也有__proto__ , 但是和实例对象的__proto__是有区别的,函数的__proto__是function这个对象的构造实例;

2、instanceof 原理

实例对象上面有一个__proto__ ,这个是引用的它构造函数的原型对象;

instanceof是用来判断实例是不是由某个构造函数实例化出来的对象,其原理是判断实例对象是否指向构造函数的原型;
只要是在原型链上的函数,都会被instanceof看做是实例对象的一个构造函数,所以都会返回true;

m1.__proto__===m1.prototype;返回true
m1.prototype.__proto===Object.prototype;返回true

o3.__proto__.constructor===Object;//返回false
所以我们判断一个实例对象的构造函数,用constructor;

3、new 运算符

后面跟着的是一个构造函数

一个新对象被创建。它继承自 foo.prototype->
构造函数foo会被执行,执行的时候,相应的传参会被传入,同时上下文(this)会被指定为这个新实例。 new foo等同于new foo(),只能在不传递任何参数的情况->
如果构造函数返回了一个‘对象’,那么这个对象会取代整个new 出来的结果。如果构造函数没有返回值, 那么new出来的结果为步骤1创建的对象

4、Object.create()

创建的实例对象是指向的对象原型,实例对象本身是不具备创建对象的属性和方法的,是通过原型链来链接的。

JS 有哪些迭代器,该如何使用?

迭代器分类

在 JavaScript 中,有三种类型的迭代器:

  • Array Iterator(数组迭代器):通过对数组进行迭代以访问其元素。

  • String Iterator(字符串迭代器):通过对字符串进行迭代以访问其字符。

  • Map Iterator(映射迭代器)和 Set Iterator(集合迭代器):通过对 Map 和 Set 数据结构进行迭代以访问其键和值。

此外,在 ES6 中,我们还可以使用自定义迭代器来迭代对象中的元素。我们可以使用 Symbol.iterator 方法来创建自定义迭代器,该方法返回一个具有 next 方法的迭代器对象。

另外,Generator 函数可以看作是一种特殊的迭代器,它能够暂停执行和恢复执行,使得我们可以通过控制迭代器的执行来生成序列。

Array Iterator(数组迭代器)有哪些迭代方法?

Array Iterator(数组迭代器)是针对 JavaScript 数组的迭代器,它可以通过 Array.prototype[Symbol.iterator]() 方法来获取。

获取到数组迭代器后,我们可以使用以下迭代方法:

next(): 返回一个包含 value 和 done 属性的对象,value 表示下一个元素的值,done 表示是否迭代结束。

return(): 用于提前终止迭代,并返回给定的值。

throw(): 用于向迭代器抛出一个异常。

下面是一个使用迭代器的示例代码:

const arr = ['a', 'b', 'c'];
const iterator = arr[Symbol.iterator]();

console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }

除了以上的迭代方法,还可以通过 for...of 语句来使用迭代器,如下所示:

const arr = ['a', 'b', 'c'];
for (const item of arr) {
  console.log(item);
}
// output:
// a
// b
// c

另外,数组迭代器除了上述的迭代方法,还可以使用 forEach()、map()、filter()、reduce() 等常见数组方法进行迭代操作;

String Iterator(字符串迭代器) 有哪些迭代方法?

String Iterator 是 ES6 引入的一种迭代器,可以用于遍历字符串。String Iterator 没有自己的迭代方法,但可以使用通用的迭代方法。以下是 String Iterator 可以使用的迭代方法:

next():返回迭代器的下一个值,格式为 {value: string, done: boolean}。
Symbol.iterator:返回一个迭代器对象,可以使用 for...of 循环来遍历字符串。

示例代码如下:

const str = "hello";
const strIterator = str[Symbol.iterator]();

console.log(strIterator.next()); // { value: 'h', done: false }
console.log(strIterator.next()); // { value: 'e', done: false }
console.log(strIterator.next()); // { value: 'l', done: false }
console.log(strIterator.next()); // { value: 'l', done: false }
console.log(strIterator.next()); // { value: 'o', done: false }
console.log(strIterator.next()); // { value: undefined, done: true }

for (let char of str) {
  console.log(char);
}
// Output:
// h
// e
// l
// l
// o

Map Iterator(映射迭代器)和 Set Iterator(集合迭代器)有哪些迭代方法?

Map Iterator 和 Set Iterator 都有以下迭代方法:
next(): 返回迭代器中下一个元素的对象,对象包含 value 和 done 两个属性。value 属性是当前元素的值,done 属性表示迭代器是否已经迭代完成。
Symbol.iterator: 返回迭代器本身,使其可被 for...of 循环使用。

Map Iterator 还有以下方法:
entries(): 返回一个新的迭代器对象,该迭代器对象的元素是 [key, value] 数组。
keys(): 返回一个新的迭代器对象,该迭代器对象的元素是 Map 中的键名。
values(): 返回一个新的迭代器对象,该迭代器对象的元素是 Map 中的键值。

Set Iterator 还有以下方法:
entries(): 返回一个新的迭代器对象,该迭代器对象的元素是 [value, value] 数组。
keys(): 返回一个新的迭代器对象,该迭代器对象的元素是 Set 中的值。
values(): 返回一个新的迭代器对象,该迭代器对象的元素是 Set 中的值。

Map Iterator 使用举例

const myMap = new Map();
myMap.set("key1", "value1");
myMap.set("key2", "value2");
myMap.set("key3", "value3");

const mapIterator = myMap.entries();

console.log(mapIterator.next().value); // ["key1", "value1"]
console.log(mapIterator.next().value); // ["key2", "value2"]
console.log(mapIterator.next().value); // ["key3", "value3"]
console.log(mapIterator.next().value); // undefined

Set Iterator 使用举例

const mySet = new Set(['apple', 'banana', 'orange']);

// 使用 for...of 循环遍历 Set
for (const item of mySet) {
  console.log(item);
}

// 使用 Set 迭代器手动遍历 Set
const setIterator = mySet.values();
let next = setIterator.next();
while (!next.done) {
  console.log(next.value);
  next = setIterator.next();
}

JSONP 是如何实现跨域的?

JSONP

JSONP 的实现原理是通过添加一个 script 标签,指定 src 属性为跨域请求的 URL,而这个 URL 返回的不是 JSON 数据,而是一段可执行的 JavaScript 代码,这段代码会调用一个指定的函数,并且将 JSON 数据作为参数传入函数中。

例如,假设我们从 http://example.com 域名下请求数据,我们可以通过在 http://example.com 中添加如下代码实现 JSONP 请求:

function handleData(data) {
  // 处理获取到的数据
}

const script = document.createElement('script');
script.src = 'http://example.org/api/data?callback=handleData';
document.head.appendChild(script);

其中,我们指定了一个名为 handleData 的回调函数,并将这个函数名作为参数传递给了跨域请求的 URL 中的 callback 参数。服务器端返回的数据将会被包装在这个回调函数中,例如:

handleData({"name": "John", "age": 30});

在这个例子中,我们可以在 handleData 函数中处理获取到的数据。需要注意的是,在使用 JSONP 时,需要保证服务器端返回的数据是一个可执行的 JavaScript 代码,并且必须使用指定的回调函数名来包装数据,否则无法正确处理数据。

如何获取 jsonp 的相应参数

获取 JSONP 响应结果的方法有两种,一种是通过回调函数参数获取另一种是通过 script 标签加载完成后解析全局变量获取

假设服务器返回以下 JSONP 响应:

callback({"name": "Alice", "age": 20});

其中 callback 是客户端定义的回调函数名,用于指定返回数据的处理方式。

我们可以使用以下两种方式获取响应结果:

1. 通过回调函数参数获取
在客户端定义一个全局函数作为回调函数,服务器返回的数据会作为回调函数的参数传入,这个参数可以在回调函数中处理。

function handleResponse(data) {
  console.log(data.name); // Alice
  console.log(data.age); // 20
}

// 创建 script 标签
const script = document.createElement('script');
script.src = 'http://example.com/api?callback=handleResponse';

// 插入到文档中开始加载数据
document.body.appendChild(script);

2. 通过全局变量获取
在客户端定义一个全局函数作为回调函数,服务器返回的数据会作为一个全局变量赋值给该函数所在的对象,我们可以在 script 标签加载完成后解析全局变量获取响应结果。

function handleResponse() {
  console.log(myData.name); // Alice
  console.log(myData.age); // 20
}

// 创建 script 标签
const script = document.createElement('script');
script.src = 'http://example.com/api?callback=handleResponse';

// 插入到文档中开始加载数据
document.body.appendChild(script);

// script 标签加载完成后解析全局变量
window.myData = {};
script.onload = () => {
  delete window.myData; // 删除全局变量
};

注意,使用 JSONP 时要注意安全问题,应该对返回的数据进行验证,避免接收到恶意代码。此外,JSONP 只能发送 GET 请求,无法发送 POST 请求,也无法使用 HTTP 请求头和请求体传递数据

ES6 Map 数据结构了解多少?

基本概念

Map 对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者基本类型)都可以作为一个键或一个值。

Map 对象是键值对的集合。Map 中的一个键只能出现一次;它在 Map 的集合中是独一无二的。Map 对象按键值对迭代——一个 for...of 循环在每次迭代后会返回一个形式为 [key,value] 的数组。迭代按插入顺序进行,即键值对按 set() 方法首次插入到集合中的顺序(也就是说,当调用 set() 时,map 中没有具有相同值的键)进行迭代。

api

静态属性

  • size 属性:size属性返回 Map 结构的成员总数。

实例方法

  • set(key, value):set方法设置key所对应的键值,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。

  • get(key):get方法读取key对应的键值,如果找不到key,返回undefined。

  • has(key):has方法返回一个布尔值,表示某个键是否在 Map 数据结构中。

  • delete(key):delete方法删除某个键,返回 true 。如果删除失败,返回 false 。

  • clear():clear方法清除所有成员,没有返回值。

  • forEach():遍历 Map 的所有成员。

迭代方法

  • keys():返回键名的遍历器。
  • values():返回键值的遍历器。
  • entries():返回所有成员的遍历器。
  • Map.prototype[@@iterator]():返回一个新的迭代对象,其为一个包含 Map 对象中所有键值对的 [key, value] 数组,并以插入 Map 对象的顺序排列。

复制或合并 Maps

Map 能像数组一样被复制:

const original = new Map([
  [1, 'one'],
]);

const clone = new Map(original);

console.log(clone.get(1)); // one
console.log(original === clone); // false. 浅比较 不为同一个对象的引用

Map 对象间可以进行合并,但是会保持键的唯一性。

const first = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

const second = new Map([
  [1, 'uno'],
  [2, 'dos']
]);

// 合并两个 Map 对象时,如果有重复的键值,则后面的会覆盖前面的。
// 展开语法本质上是将 Map 对象转换成数组。
const merged = new Map([...first, ...second]);

console.log(merged.get(1)); // uno
console.log(merged.get(2)); // dos
console.log(merged.get(3)); // three

Map 对象也能与数组合并:

const first = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three'],
]);

const second = new Map([
  [1, 'uno'],
  [2, 'dos']
]);

// Map 对象同数组进行合并时,如果有重复的键值,则后面的会覆盖前面的。
const merged = new Map([...first, ...second, [1, 'eins']]);

console.log(merged.get(1)); // eins
console.log(merged.get(2)); // dos
console.log(merged.get(3)); // three

实现一个双向链表, 具备添加节点、删除节点、在特定位置插入节点、查找节点、遍历等功能

必须要掌握的知识

在 JavaScript 中实现双向链表需要掌握以下知识点:

  • 如何使用构造函数和类创建双向链表节点,以及如何在节点之间建立双向连接。

  • 双向链表的常用操作,包括添加节点、删除节点、在特定位置插入节点、查找节点等。

  • 双向链表的遍历和迭代,包括正向遍历、反向遍历、循环遍历等。

  • 链表的常见问题,例如链表是否为空、链表长度、查找节点等。

  • 对 JavaScript 垃圾回收机制的理解,确保双向链表的实现不会导致内存泄漏。

以上知识点是实现双向链表所必须掌握的内容,掌握这些知识点能够帮助我们有效地创建和操作双向链表。

什么是双向链表

双向链表(Doubly linked list)是一种常见的数据结构,它是由一系列节点组成的,每个节点都包含一个指向前驱节点和后继节点的指针。相比单向链表,双向链表具有双向遍历的能力,即可以从任意一个节点开始,向前或向后遍历整个链表。

双向链表的每个节点通常包含两个指针,即 prev 指针和 next 指针。prev 指针指向当前节点的前驱节点,而 next 指针指向当前节点的后继节点。由于每个节点都包含两个指针,因此双向链表的节点通常比单向链表的节点更占用空间。

双向链表可以用于实现各种数据结构和算法,如LRU(Least Recently Used)缓存淘汰算法,双向队列(Deque)等。由于它具有双向遍历的能力,因此在某些场景下可以比单向链表更加高效和方便。

实现一个双向链表

class Node {
  constructor(value) {
    this.value = value;
    this.next = null;
    this.prev = null;
  }
}

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.length = 0;
  }

  // 在链表末尾添加节点
  push(value) {
    const node = new Node(value);
    if (this.length === 0) {
      this.head = node;
      this.tail = node;
    } else {
      this.tail.next = node;
      node.prev = this.tail;
      this.tail = node;
    }
    this.length++;
    return this;
  }

  // 从链表末尾移除节点
  pop() {
    if (this.length === 0) {
      return undefined;
    }
    const node = this.tail;
    if (this.length === 1) {
      this.head = null;
      this.tail = null;
    } else {
      this.tail = node.prev;
      this.tail.next = null;
      node.prev = null;
    }
    this.length--;
    return node.value;
  }

  // 在链表开头添加节点
  unshift(value) {
    const node = new Node(value);
    if (this.length === 0) {
      this.head = node;
      this.tail = node;
    } else {
      this.head.prev = node;
      node.next = this.head;
      this.head = node;
    }
    this.length++;
    return this;
  }

  // 从链表开头移除节点
  shift() {
    if (this.length === 0) {
      return undefined;
    }
    const node = this.head;
    if (this.length === 1) {
      this.head = null;
      this.tail = null;
    } else {
      this.head = node.next;
      this.head.prev = null;
      node.next = null;
    }
    this.length--;
    return node.value;
  }

  // 获取指定位置的节点
  get(index) {
    if (index < 0 || index >= this.length) {
      return undefined;
    }
    let node = null;
    if (index < this.length / 2) {
      node = this.head;
      for (let i = 0; i < index; i++) {
        node = node.next;
      }
    } else {
      node = this.tail;
      for (let i = this.length - 1; i > index; i--) {
        node = node.prev;
      }
    }
    return node;
  }

  // 在指定位置插入节点
  insert(index, value) {
    if (index < 0 || index > this.length) {
      return false;
    }
    if (index === 0) {
      return !!this.unshift(value);
    }
    if (index === this.length) {
      return !!this.push(value);
    }
    const node = new Node(value);
    const prevNode = this.get(index - 1);
    const nextNode = prevNode.next;
    prevNode.next = node;
    node.prev = prevNode;
    node.next = nextNode;
    nextNode.prev = node;
    this.length++;
    return true;
  }

  // 移除指定位置的节点
  remove(index) {
    if (index < 0 || index >= this.length) {
      return undefined;
    }
    if (index === 0) {
      return this.shift();
    }
    if (index === this.length - 1) {
      return this.pop();
    }
    const nodeToRemove = this.get(index);
    const prevNode = nodeToRemove.prev;
    const nextNode = nodeToRemove.next;
    prevNode.next = nextNode;
    nextNode.prev = prevNode;
    nodeToRemove.next = null;
    nodeToRemove.prev = null;
    this.length--;
    return nodeToRemove.value;
  }

  // 反转链表
  reverse() {
    let node = this.head;
    this.head = this.tail;
    this.tail = node;
    let prevNode = null;
    let nextNode = null;
    for (let i = 0; i < this.length; i++) {
      nextNode = node.next;
      node.next = prevNode;
      node.prev = nextNode;
      prevNode = node;
      node = nextNode;
    }
    return this;
  }

  // 通过 value 来查询 index
  findIndexByValue(value) {
    let currentNode = this.head;
    let index = 0;

    while (currentNode) {
      if (currentNode.value === value) {
        return index;
      }
      currentNode = currentNode.next;
      index++;
    }

    return -1; // 如果链表中没有找到该值,返回 -1
  }

  // 正向遍历链表,并返回遍历结果
  forwardTraversal() {
    const result = [];
    let current = this.head;
    while (current) {
      result.push(current.value);
      current = current.next;
    }
    return result;
  }

  // 反向遍历链表,并返回遍历结果
  backwardTraversal() {
    const result = [];
    let current = this.tail;
    while (current) {
      result.push(current.value);
      current = current.prev;
    }
    return result;
  }

  // 循环遍历链表,并返回遍历结果
  loopTraversal() {
    const result = [];
    let current = this.head;
    while (current) {
      result.push(current.value);
      current = current.next;
      if (current === this.head) {
        break;
      }
    }
    return result;
  }
}

如何使对象 iterable 化, 以其可以支持 for...of 迭代

在 JavaScript 中,如果一个对象要被 for...of 迭代,那么它必须是可迭代的。可迭代对象是一种具有 Symbol.iterator 方法的对象,该方法返回一个迭代器对象,该迭代器对象实现了 next() 方法,每次调用 next() 方法都返回一个包含 value 和 done 属性的对象,用于迭代对象的每个元素。

因此,要使一个对象 iterable 化,需要实现一个 Symbol.iterator 方法。该方法应该返回一个迭代器对象,这个迭代器对象应该实现 next() 方法,用于返回迭代对象的每个元素。

举一个例子下面是一个简单的示例,演示如何将一个普通对象 iterable 化:

const myObj = {
  data: [1, 2, 3],
  [Symbol.iterator]() {
    let index = 0;
    return {
      next: () => {
        if (index < this.data.length) {
          return { value: this.data[index++], done: false };
        } else {
          return { done: true };
        }
      },
    };
  },
};

for (const item of myObj) {
  console.log(item);
}
// 输出:1, 2, 3

再举一个例子,比如我们有一个对象,里面存储了一些学生的信息,我们希望能够使用 for...of 循环遍历每个学生信息:

const students = {
  Alice: { age: 18, gender: 'female', score: 90 },
  Bob: { age: 19, gender: 'male', score: 85 },
  Charlie: { age: 20, gender: 'male', score: 95 }
};

students[Symbol.iterator] = function* () {
  const keys = Object.keys(this);
  for (let i = 0; i < keys.length; i++) {
    yield [keys[i], this[keys[i]]];
  }
};

for (const [name, info] of students) {
  console.log(`${name}: ${info.age} ${info.gender} ${info.score}`);
}

这样我们就可以使用 for...of 循环遍历学生信息对象中的每个学生信息了。

解释边距重叠

什么是BFC

BFC (block formatting context) 及块级格式化上下文,从样式上看,具有 BFC 的元素与普通的容器没有什么区别,从功能上看,BFC相当于构建了一个密闭的盒子模型,在BFC中的元素不受外部元素的影响;

个人理解:BFC就是将盒子中子元素的属性锁在父元素中,例如margin,float 使其不影响盒子外的元素。

如何构建BFC

以下情况都会使元素产生BFC

  • 根元素或其它包含它的元素 (也就是html元素本身就是BFC)
  • float:left ,right
  • position:absolute,fixed
  • display:inline-block,table-cell,table-caption;(行内块元素与表格元素)
  • overflow:hidden,auto,scroll (非 visible属性)
  • display: flow-root
  • column-span: all

BFC的作用

1. 解决高度塌陷

由于浮动元素脱离了文档流,普通盒子是无法包裹住已经浮动的元素;父级元素的高度为0;

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        * { box-sizing: border-box; }

        .outer {
            background-color: #ccc;
            width: 200px;
        }
        .outer div{
            width: 100px;
            margin: 10px 20px;
            background-color: red;
            width: 100px;
            height: 100px;
        }

    </style>
</head>
<body >
    <div class="outer ">
        <div style="float: left;"></div>
    </div>
</body>
</html>

当子元素浮动 父级获取不到浮动元素的高度,造成高度塌陷

当父元素转变为BFC时,浮动元素被包裹住:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        * { box-sizing: border-box; }

        .outer {
            background-color: #ccc;
            width: 200px;
            overflow: hidden;  //转变为BFC
        }
        .outer div{
            width: 100px;
            margin: 10px 20px;
            background-color: red;
            width: 100px;
            height: 100px;
        }

    </style>
</head>
<body >
    <div class="outer ">
        <div style="float: left;"></div>
    </div>
</body>
</html>

2.浮动重叠

当一个元素浮动,后面的元素没浮动,那么后面的元素就会与浮动元素发生重叠

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        * { box-sizing: border-box; }

        .outer {
            background-color: #ccc;
            width: 200px;
            overflow: hidden;
        }
        .outer div{
            width: 100px;
            margin: 10px 20px;
            background-color: red;
            width: 100px;
            height: 100px;
        }

    </style>
</head>
<body >
    <div class="outer ">
        <div style="float: left;"></div>
        <div ></div>
    </div>
</body>
</html>

后一个元素 与前一个浮动元素发生重叠

根据BFC不与浮动元素重叠的特性,为没有浮动的元素创建BFC环境

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        * { box-sizing: border-box; }

        .outer {
            background-color: #ccc;
            width: 200px;
            overflow: hidden;
        }
        .outer div{
            width: 100px;
            margin: 10px 20px;
            background-color: red;
            width: 100px;
            height: 100px;
        }

    </style>
</head>
<body >
    <div class="outer ">
        <div style="float: left;"></div>
        <div style="overflow: hidden;"></div>
    </div>
</body>
</html>

3.边距重叠

边距重叠分为两种情况

  • 父子重叠
当 父级没有 
-  垂直方向的border,
-  垂直方向 padding,
-  父级不是内联元素,
-  父级不是BFC,
-  父级没有清除浮动,

这五个条件时,子元素的上下边距会和父级发生重叠 
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        * { box-sizing: border-box; }

        .outer {
            background-color: #ccc;
            width: 200px;
        }
        .outer div{
            width: 100px;
            margin: 10px 20px;
            background-color: red;
            width: 100px;
            height: 100px;
        }

    </style>
</head>
<body >
    <div class="outer ">
        <div></div>
    </div>
</body>
</html>

解决办法:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        * { box-sizing: border-box; }

        .outer {
            background-color: #ccc;
            width: 200px;
            /*padding: 1px;*/    加padding  
            /*border: 1px solid yellow;*/ 加border
            /*display: inline-block;*/  内联块
            /*overflow: hidden;*/       BFC
        }
        .clearfix:after{                清除浮动
            content: '';
            display: table;
            clear:both;
        }
        .outer div{
            width: 100px;
            margin: 10px 20px;
            background-color: red;
            width: 100px;
            height: 100px;
        }

    </style>
</head>
<body >
    <div class="outer clearfix">
        <div></div>
    </div>
</body>
</html>
  • 兄弟重叠
    当两个元素的垂直边距相互接触时,两者边距会发生合并,合并的规则为
- 如果是正数比大小,大的覆盖小的
- 都为负数比绝对值大小,大的覆盖小的
- 正负都有取其差

1.将两个元素浮动
2.将两个元素display:inline-block
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        * { box-sizing: border-box; }

        .outer {
            background-color: #ccc;
            width: 200px;
            overflow: hidden;
        }
        .outer div{
            width: 100px;
            margin: 10px 20px;
            background-color: red;
            width: 100px;
            height: 100px;
            /*下面两种方式*/
            float: left;
            display: inline-block;
        }
    </style>
</head>
<body >
    <div class="outer ">
        <div ></div>
        <div ></div>
    </div>
</body>
</html>

其实兄弟重叠完全可以设置一个最大值的边距就可达到想要的效果,完全没有必要去使用上面的两个方法。

参考文档

HTTP建立连接的过程?

在HTTP/1.1中

建立连接过程遵循以下步骤:

  • 建立TCP连接:客户端通过三次握手建立TCP连接。

  • 发送请求:客户端向服务器发送一个HTTP请求报文。

  • 服务器响应:服务器收到请求后,返回一个HTTP响应报文。

  • 客户端接收响应:客户端收到响应后,根据响应中的状态码判断请求是否成功。

  • 关闭连接:如果响应中包含 Connection: close 头部,那么连接关闭,否则保持连接,可以继续发送请求。

在HTTP/2中

建立连接过程使用了多路复用,可以在一个连接上同时处理多个请求和响应,具体过程如下:

  • 客户端和服务器建立TCP连接。

  • 客户端发送一个HTTP/2的SETTINGS帧,其中包含一些配置信息,如帧的大小和流的并发数量等。

  • 服务器返回一个HTTP/2的SETTINGS帧,确认了客户端发送的设置。

  • 客户端发送一个HTTP/2的HEADERS帧,其中包含了第一个请求的信息,同时还包含了一个唯一的标识符,称为流ID。

  • 服务器返回一个HTTP/2的HEADERS帧,其中包含了响应的信息,同时也包含了与请求相同的流ID。

  • 客户端可以在同一个连接上发送多个请求和响应,每个请求和响应都包含一个流ID,用于标识请求和响应之间的关系。

  • 当客户端或服务器想要关闭连接时,它可以发送一个HTTP/2的GOAWAY帧,表示不再接受新的请求或响应,并且将连接关闭。

总之,HTTP/1.1是基于请求-响应模型的,每次请求都需要建立一个新的连接。而HTTP/2使用多路复用,可以在一个连接上处理多个请求和响应,提高了性能和效率。

手写创建一个 ajax 请求

手动创建一个 ajax 请求

一般来说,我们可以使用XMLHttpRequest对象来创建Ajax请求,其流程如下:

  1. 创建XMLHttpRequest对象,通过调用其构造函数来实现。
  2. 使用open()方法指定请求的方法、URL以及是否异步请求。
  3. 使用setRequestHeader()方法设置请求头,例如设置请求的Content-Type。
  4. 设置响应的回调函数,一般有onreadystatechange和onload两种方式。
  5. 使用send()方法发送请求。

实现如下:

var getJSON = function(url) {
  var promise = new Promise(function(resolve, reject) {
    function handler() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    }

    var client = new XMLHttpRequest();
    //如果是IE的内核ActiveXObject('Microsoft.XMLHTTP');
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    //如果是post请求:client.setRequestHeader('Content-Type','application/X-WWW-form-urlencoded')
    client.send();
  });
  return promise;
};

getJSON("/posts.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error(' 出错了 ', error);
});
  • xhr.open() 第一个参数是请求的方法,可以是GET、POST、PUT等;第二个参数是请求的URL;第三个参数表示是否异步请求。

  • setRequestHeader()方法用于设置请求头,例如设置Content-Type,常见的值有application/json、application/x-www-form-urlencoded等

  • onreadystatechange回调函数会在XMLHttpRequest对象的状态发生变化时触发

  • 最后,调用send()方法发送请求。

回调函数一般有哪些?

设置响应的回调函数,一般有 onreadystatechangeonload 两种方式;

在 XMLHttpRequest 对象中,onreadystatechange 和 onload 是两种不同的事件回调函数。

onreadystatechange :事件会在 readyState 的值改变时被触发,它会在请求过程中的每个状态改变时都被触发,从而可以通过 readyState
的值来判断请求的过程。一般来说,onreadystatechange 回调函数需要根据 readyState 的不同值做出不同的处理,如:

  • readyState 为 1 (UNSENT):代理被创建,但尚未调用 open() 方法;
  • readyState 为 1 (已经调用 open() 方法)时,可以做一些请求初始化的工作;
  • readyState 为 2 (已经调用 send() 方法)时,可以获取响应头信息;
  • readyState 为 3 (正在接收数据)时,可以获取响应的部分数据;
  • readyState 为 4 (已经接收到全部响应数据)时,可以对响应的数据进行处理。

onload: 而 onload 事件则是在整个请求过程完成后被触发,表示整个请求已经完成。这个回调函数通常用来处理响应数据,如将响应数据渲染到页面中等。

因此,onreadystatechange 和 onload 这两种回调函数的作用是不同的,需要根据不同的场景进行选择和使用。

let 和 const 与 var 的区别?

let 和 const 与 var 的区别

1、不存在变量提升
必须先定义后使用,否则报错

2、暂时性死区
在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

3、不允许重复申明/不允许在函数内部重新申明参数(也算重复申明)

4.1 SE5的作用域
1)、内层变量覆盖外层的变量
2)、用来计数的循环变量会泄露为全局变量

5、const是一个常量,一旦申明,就不能改变。而且在申明的时候必须初始化,不能留到后面赋值。

6、在ES5里面,未申明的全局变量会自动生为window的属性:
没法在编译过程爆出变量为申明的错误,语法上顶层对象有一个实体含义的对象这样肯定不合适。
用var定义的依然会升级为顶层对象(全局对象)window的属性;但是let,const申明则不会。

水平垂直居中定位

水平垂直居中定位

垂直居中的方案

1、

line-height: 200px;
vertical-align: middle;

2、CSS Table

#parent {display: table;}
#child {
display: table-cell;
vertical-align: middle;
}

3、Absolute Positioning and Negative Margin

#parent {position: relative;}
#child {
    position: absolute;
    top: 50%;
    left: 50%;
    height: 30%;
    width: 50%;
    margin: -15% 0 0 -25%;
}

4、Absolute Positioning and Stretching

#parent {position: relative;}
#child {
position: absolute;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    width: 50%;
    height: 30%;
    margin: auto;
}

5、Equal Top and Bottom Padding

#parent {
    padding: 5% 0;
}
#child {
    padding: 10% 0;
}

水平居中的方案

1、要实现行内元素(、等)的水平居中:text-align:center;

2、要实现块状元素(display:block)的水平居中: margin:0 auto;

3、多个水平排列的块状元素的水平居中:

#container{
    text-align:center;
}
#center{
    display:inline-block;
}

4、flexbox

#container {
    display: flex;
}
#container {
    display: inline-flex;
}

5、一直宽度水平居中:绝对定位与负边距实现。

#container{
    position:relative;
}

#center{
    width:100px;
    height:100px;
    position:absolute;
    top:50%;
    left:50%;
    margin:-50px 0 0 -50px;
}

6、绝对定位与margin:

#container{
    position:relative;
}
#center{
    position:absolute;
    margin:auto;
    top:0;
    bottom:0;
    left:0;
    right:0;
}

未知高度和宽度元素的水平垂直居中

1、当要被居中的元素是inline或者inline-block元素

 #container{
    display:table-cell;
    text-align:center;
    vertical-align:middle;
}

#center{

}

2、利用Css3的transform,可以轻松的在未知元素的高宽的情况下实现元素的垂直居中。

#container{
    position:relative;
}
#center{
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
}

3、flex

#container{
    display:flex;
    justify-content:center;
    align-items: center;
}

#center{

}

常见数组排序算法有哪些?

常见排序算法中,时间复杂度和空间复杂度是怎样的?

如图所示:
01_10

快速排序:

先从数列中取出一个数作为“基准”。

分区过程:将比这个“基准”大的数全放到“基准”的右边,小于或等于“基准”的数全放到“基准”的左边。
再对左右区间重复第二步,直到各区间只有一个数。

var quickSort = function(arr) {
    if (arr.length <= 1) { return arr; }
    var pivotIndex = Math.floor(arr.length / 2);   //基准位置(理论上可任意选取)
    var pivot = arr.splice(pivotIndex, 1)[0];  //基准数
    var left = [];
    var right = [];
    for (var i = 0; i < arr.length; i++){
        if (arr[i] < pivot) {
            left.push(arr[i]);
        } else {
            right.push(arr[i]);
        }
    }
    return quickSort(left).concat([pivot], quickSort(right));  //链接左数组、基准数构成的数组、右数组
};

选择排序:

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置
再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
重复第二步,直到所有元素均排序完毕。

function selectionSort(arr) {
    var len = arr.length;
    var minIndex, temp;
    for (var i = 0; i < len - 1; i++) {
        minIndex = i;
        for (var j = i + 1; j < len; j++) {
            if (arr[j] < arr[minIndex]) {     // 寻找最小的数
                minIndex = j;                 // 将最小数的索引保存
            }
        }
        temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    return arr;
}

插入排序:

将第一待排序序列第一个元素看做一个有序序列,把第二个元素到最后一个元素当成是未排序序列。
从头到尾依次扫描未排序序列,将扫描到的每个元素插入有序序列的适当位置。(如果待插入的元素与有序序列中的某个元素相等,则将待插入元素插入到相等元素的后面。)

function insertionSort(arr) {
    var len = arr.length;
    var preIndex, current;
    for (var i = 1; i < len; i++) {
        preIndex = i - 1;
        current = arr[i];
        while(preIndex >= 0 && arr[preIndex] > current) {
            arr[preIndex+1] = arr[preIndex];
            preIndex--;
        }
        arr[preIndex+1] = current;
    }
    return arr;
}

冒泡法排序:

比较相邻的元素。如果第一个比第二个大,就交换他们两个。
对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。
针对所有的元素重复以上的步骤,除了最后一个。
持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len - 1; i++) {
        for (var j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j+1]) {        // 相邻元素两两对比
                var temp = arr[j+1];        // 元素交换
                arr[j+1] = arr[j];
                arr[j] = temp;
            }
        }
    }
    return arr;
}

希尔排序

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。
它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序。

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
按增量序列个数k,对序列进行k 趟排序;
每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。
仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

function shellSort(arr) {
    var len = arr.length,
        temp,
        gap = 1;
    while (gap < len / 3) {          // 动态定义间隔序列
        gap = gap * 3 + 1;
    }
    for (gap; gap > 0; gap = Math.floor(gap / 3)) {
        for (var i = gap; i < len; i++) {
            temp = arr[i];
            for (var j = i-gap; j > 0 && arr[j]> temp; j-=gap) {
                arr[j + gap] = arr[j];
            }
            arr[j + gap] = temp;
        }
    }
    return arr;
}

归并排序

直接上代码了

function mergeSort(arr){
    var len = arr.length;
    if(len <2)
        return arr;
    var mid = Math.floor(len/2),
        left = arr.slice(0,mid),
        right =arr.slice(mid);
    //send left and right to the mergeSort to broke it down into pieces
    //then merge those
    return merge(mergeSort(left),mergeSort(right));
}

function merge(left, right){
    var result = [],
        lLen = left.length,
        rLen = right.length,
        l = 0,
        r = 0;
    while(l < lLen && r < rLen){
        if(left[l] < right[r]){
            result.push(left[l++]);
        }
        else{
            result.push(right[r++]);
        }
    }
    //remaining part needs to be addred to the result
    return result.concat(left.slice(l)).concat(right.slice(r));
}

call、apply、bind 的区别和用法?

call、apply 和 bind 都是 JavaScript 中用于改变函数执行上下文(即 this 指向)的方法,它们的区别和用法如下:

call

call 方法可以改变函数的 this 指向,同时还能传递多个参数。
call 方法的语法如下:

fun.call(thisArg, arg1, arg2, ...)

fun:要调用的函数。
thisArg:函数内部 this 指向的对象。
arg1, arg2, ...:传递给函数的参数列表。

call 方法的使用示例:

const person = {
  name: 'Alice',
  sayHello: function () {
    console.log(`Hello, ${this.name}!`)
  },
}

const person2 = {
  name: 'Bob',
}

person.sayHello.call(person2) // 输出:Hello, Bob!

apply

apply 方法和 call 方法类似,它也可以改变函数的 this 指向,但是它需要传递一个数组作为参数列表。
apply 方法的语法如下:

fun.apply(thisArg, [argsArray])

fun:要调用的函数。
thisArg:函数内部 this 指向的对象。
argsArray:传递给函数的参数列表,以数组形式传递。

apply 方法的使用示例:

const person = {
  name: 'Alice',
  sayHello: function (greeting) {
    console.log(`${greeting}, ${this.name}!`)
  },
}

const person2 = {
  name: 'Bob',
}

person.sayHello.apply(person2, ['Hi']) // 输出:Hi, Bob!

bind

bind 方法和 call、apply 方法不同,它并不会立即调用函数,而是返回一个新的函数,这个新函数的 this 指向被绑定的对象。
bind 方法的语法如下:

fun.bind(thisArg[, arg1[, arg2[, ...]]])

fun:要调用的函数。
thisArg:函数内部 this 指向的对象。
arg1, arg2, ...:在调用函数时,绑定到函数参数的值。

bind 方法的使用示例:

const person = {
  name: 'Alice',
  sayHello: function () {
    console.log(`Hello, ${this.name}!`)
  },
}

const person2 = {
  name: 'Bob',
}

const sayHelloToBob = person.sayHello.bind(person2)
sayHelloToBob() // 输出:Hello, Bob!

参数传递
在使用 bind 方法时,我们可以通过传递参数来预先填充函数的一些参数,这样在调用函数时只需要传递剩余的参数即可。

const person = {
  name: 'Alice',
  sayHello: function (greeting, punctuation) {
    console.log(`${greeting}, ${this.name}, ${punctuation}`)
  },
}

const person2 = {
  name: 'Bob',
}

const sayHelloToBob = person.sayHello.bind(person2);

sayHelloToBob(1,2); // 输出:1, Bob, 2

再举一个例子:

this.x = 9;    // 在浏览器中,this 指向全局的 "window" 对象
var module = {
  x: 81,
  getX: function() { return this.x; }
};

module.getX(); // 81

var retrieveX = module.getX;
retrieveX();
// 返回 9 - 因为函数是在全局作用域中调用的

// 创建一个新函数,把 'this' 绑定到 module 对象
// 新手可能会将全局变量 x 与 module 的属性 x 混淆
var boundGetX = retrieveX.bind(module);
boundGetX(); // 81

HTTP 缓存策略有哪些?

HTTP 缓存策略有哪些?

HTTP缓存策略是指浏览器和服务器之间在传输资源时,如何使用缓存的方式。HTTP缓存的主要目的是减少网络传输的数据量,提高页面的访问速度。

缓存的主要策略有哪些?

HTTP缓存策略主要包括以下几种:

  • 强缓存:通过设置 HTTP 头部中的 Expires 或 Cache-Control 字段来指定资源在本地缓存的有效期。当资源未过期时,浏览器直接从缓存中读取,不会向服务器发送请求,从而提高页面的访问速度。

  • 协商缓存:当资源的缓存时间已经过期,浏览器会向服务器发送请求,服务器会检查资源是否有更新,如果没有更新,则返回 304 状态码,告诉浏览器直接使用本地缓存。

    • Last-Modified / If-Modified-Since:服务器在返回资源时,会添加 Last-Modified 头部字段,表示资源最后的修改时间。当浏览器下次请求该资源时,会在请求头部添加 If-Modified-Since 字段,表示上次请求时资源的修改时间。服务器检查这两个时间是否一致,如果一致,则返回 304 状态码,否则返回新的资源。
    • ETag / If-None-Match:服务器在返回资源时,会添加 ETag 头部字段,表示资源的唯一标识。当浏览器下次请求该资源时,会在请求头部添加 If-None-Match 字段,表示上次请求时资源的唯一标识。服务器检查这两个标识是否一致,如果一致,则返回 304 状态码,否则返回新的资源。
  • 离线缓存:通过使用 HTML5 提供的 Application Cache API,可以将页面的资源缓存在本地,使得用户在没有网络连接的情况下也能够访问页面。

  • Service Worker 缓存:Service Worker 是一种在浏览器后台运行的 JavaScript 线程,可以拦截和处理浏览器发送的网络请求。通过使用 Service Worker,可以将页面的资源缓存在本地,提高页面的访问速度和用户体验。

强缓存中 Expires 或 Cache-Control 有什么区别?

在 HTTP 缓存策略中,强缓存是指在一定时间内,直接使用本地缓存而不发送请求到服务器。Expires 和 Cache-Control 是用于设置强缓存的两种方式。

  • Expires: 是 HTTP/1 的产物,它是一个 HTTP 头字段,表示资源过期时间,是一个绝对时间。服务器返回的 HTTP 头中,如果包含 Expires 字段,则表示该资源在该过期时间之前可以直接从缓存中获取,而不需要再次请求服务器。
  • Cache-Control: 是 HTTP/1.1 的产物,是一个 HTTP 头字段,用来控制文档缓存行为。它的值可以是很多不同的指令,例如 max-age、no-cache、no-store、must-revalidate 等等。其中,max-age 指令可以设置资源的最大有效时间,单位是秒。如果服务器返回的 HTTP 头中包含 Cache-Control 指令,则浏览器会根据该指令的值来决定是否直接使用本地缓存,而不需要再次请求服务器。

Expires 是一个绝对时间,因此它的缺点是当服务器的时间与客户端的时间不一致时,缓存过期时间就可能会出现偏差。
而 Cache-Control 是一个相对时间,因此它的缺点是需要服务器和客户端的时间保持一致,同时需要正确设置 max-age 的值。
在实际应用中,建议使用 Cache-Control,因为它更加灵活和可控。

离线缓存 Application Cache API 是如何缓存 http 资源的?

Application Cache API(应用程序缓存)是 HTML5 标准中提供的一个用于离线缓存 Web 应用程序的技术。它可以将 Web 应用程序中的文件(包括 HTML、CSS、JavaScript 和图像等)保存到客户端浏览器中的缓存中,在没有网络连接的情况下,仍然能够访问应用程序。

在 Application Cache API 中,通过在 cache manifest 文件中列出需要缓存的资源列表来实现离线缓存。该文件必须以 .appcache 为后缀名,并且必须在 Web 服务器上进行访问。浏览器会下载该文件,并将文件中列出的资源文件下载到本地缓存中。当应用程序在离线状态下打开时,浏览器会自动从本地缓存中加载缓存的文件。

下面是一个简单的 cache manifest 文件示例:

CACHE MANIFEST
# version 1.0.0

CACHE:
index.html
styles.css
script.js
image.jpg

NETWORK:
*

FALLBACK:

上面的示例文件将缓存 index.html、styles.css、script.js 和 image.jpg 等资源文件,同时指定 NETWORK 和 FALLBACK,这两个属性分别用于指定离线缓存不生效时的网络连接策略和替换资源文件。

需要注意的是,Application Cache API 并不是一种完美的缓存技术,它也存在一些缺陷。例如,当更新 Web 应用程序时,需要手动清除客户端浏览器中的缓存才能生效,否则用户访问的仍然是旧版本的应用程序。同时,Application Cache API 只能缓存 GET 请求,不支持 POST 等其他请求方法。因此,为了更好地实现离线缓存,可以使用其他技术,例如 Service Worker。

Service Worker 是如何缓存 http 请求资源的?

ervice Worker 是一种在浏览器后台运行的脚本,可以拦截和处理浏览器网络请求。因此,可以使用 Service Worker 来缓存 http 请求资源。

Service Worker 可以通过以下步骤来缓存 http 请求资源:

  1. 注册 Service Worker:通过在页面中注册 Service Worker,可以告诉浏览器使用 Service Worker 来处理网络请求。

  2. 安装 Service Worker:一旦 Service Worker 被注册,浏览器就会下载并安装它。在安装过程中,Service Worker 可以缓存一些静态资源(如 HTML、CSS 和 JavaScript 文件)。

  3. 激活 Service Worker:一旦 Service Worker 安装成功,它就可以被激活。在激活过程中,Service Worker 可以删除旧版本的缓存,或者执行其他一些操作。

  4. 拦截网络请求:一旦 Service Worker 被激活,它就可以拦截浏览器发送的网络请求。

  5. 处理网络请求:当 Service Worker 拦截到网络请求时,它可以执行一些自定义的逻辑来处理这些请求。例如,它可以检查缓存中是否已经存在该请求的响应,如果存在,则直接返回缓存中的响应,否则,它可以将请求发送到服务器并缓存服务器的响应。

  6. 更新缓存:如果缓存中的资源发生了变化,Service Worker 可以自动更新缓存。例如,它可以在后台下载最新的资源,并更新缓存中的文件。

需要注意的是,使用 Service Worker 来缓存 http 请求资源需要一些额外的工作。例如,需要编写 Service Worker 脚本来处理请求,并且需要将该脚本注册到浏览器中。此外,还需要考虑一些缓存策略,以确保缓存的数据与服务器上的数据保持同步。

下面是一个使用 Service Worker 实现缓存的示例代码:

// 注册 Service Worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', function() {
    navigator.serviceWorker.register('/service-worker.js').then(function(registration) {
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    }, function(err) {
      console.log('ServiceWorker registration failed: ', err);
    });
  });
}

// 安装 Service Worker
self.addEventListener('install', function(event) {
  console.log('ServiceWorker install');
  event.waitUntil(
    caches.open('my-cache').then(function(cache) {
      return cache.addAll([
        '/',
        '/index.html',
        '/styles.css',
        '/script.js',
        '/image.png'
      ]);
    })
  );
});

// 激活 Service Worker
self.addEventListener('activate', function(event) {
  console.log('ServiceWorker activate');
});

// 拦截网络请求
self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.match(event.request).then(function(response) {
      if (response) {
        console.log('ServiceWorker fetch from cache:', event.request.url);
        return response;
      } else {
        console.log('ServiceWorker fetch from network:', event.request.url);
        return fetch(event.request);
      }
    })
  );
});

// 更新缓存
self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys().then(cacheNames => {
      return Promise.all(
        cacheNames.filter(cacheName => {
          return cacheName.startsWith('my-cache') &&
            cacheName !== 'my-cache';
        }).map(cacheName => {
          return caches.delete(cacheName);
        })
      );
    })
  );
});

当网络请求到来时,会首先在缓存中查找对应的资源,如果有则直接返回缓存中的资源,否则从网络中获取资源并返回。这样就可以实现基本的离线缓存功能。

在这个示例中,当 Service Worker 被安装时,我们打开一个新的缓存并将应用程序的静态资源添加到缓存中。在 fetch 事件中,我们拦截每个网络请求并尝试匹配它们到我们的缓存中,如果匹配到了则返回缓存的响应,否则通过 fetch 方法从网络中获取资源。在 activate 事件中,我们可以更新缓存,删除旧的缓存项并将新的缓存项添加到缓存中。

JS数据类型有哪些,区别是什么?

在 JavaScript 中,数据类型可以分为两类:原始类型和对象类型。原始类型包括:数字(number)、字符串(string)、布尔值(boolean)、null、undefined 和 Symbol(ES6 新增),对象类型包括:对象(object)、数组(array)、函数(function)等。

区别如下:

  • 原始类型的值是不可变的,对象类型的值是可变的。
  • 原始类型的值是按值访问的,对象类型的值是按引用访问的。
  • 原始类型存储在栈内存中,对象类型存储在堆内存中。

原始类型
具体来说,数字、字符串、布尔值、null 和 undefined 是 JavaScript 中的五种原始类型,它们都是不可变的。每次对原始类型进行操作时,都会创建一个新的原始类型的值。例如:

let num1 = 10;
let num2 = num1 + 5;
console.log(num1); // 10
console.log(num2); // 15

在上面的例子中,对 num1 进行操作时并没有改变 num1 的值,而是创建了一个新的值 num2。

对象类型
对象类型则是可变的,因为对象、数组、函数等值是通过引用来访问的。例如:

let obj1 = { name: '张三' };
let obj2 = obj1;
obj2.name = '李四';
console.log(obj1.name); // "李四"
console.log(obj2.name); // "李四"

在上面的例子中,修改了 obj2 的属性值,但由于 obj1 和 obj2 指向的是同一个对象,所以 obj1 的属性值也被修改了。

Symbol
除了五种原始类型和对象类型外,ES6 新增了一种原始类型:Symbol。它的主要作用是创建唯一的标识符。例如:

let s1 = Symbol();
let s2 = Symbol();
console.log(s1 === s2); // false

在上面的例子中,两个 Symbol 创建的值是不相等的,即使它们的值是一样的。

postMessage 是如何解决跨域问题的?

基本概念

postMessage 是 HTML5 提供的一种跨窗口通信机制,可以在不同窗口、甚至不同域名之间进行通信,从而实现跨域通信。通过在一个窗口中调用 postMessage 方法向另一个窗口发送消息,接收窗口可以监听 message 事件,以接收发送过来的消息。

使用 postMessage 可以解决一些跨域问题,如在一个域名下的网页与其他域名下的网页进行通信。具体来说,当两个窗口的协议、主机名或端口不同时,就会发生跨域问题。但使用 postMessage 可以突破同源策略的限制,实现不同域之间的通信。

一般情况下,为了保证安全,使用 postMessage 进行跨域通信时,需要在目标窗口中设置 window.postMessage 的接收地址,只有来自该地址的消息才能被接收,从而避免了安全问题。同时,可以使用 origin 参数来限制消息来源,避免恶意攻击。

代码举例

假设我们有两个域名为 https://domain-a.comhttps://domain-b.com 的网站,现在需要在这两个网站之间进行跨域通信。

https://domain-a.com 的页面中,我们可以使用以下代码向 https://domain-b.com 发送一条消息:

const targetOrigin = "https://domain-b.com";
const message = "Hello, domain-b!";

window.parent.postMessage(message, targetOrigin);

这里的 window.parent 表示当前页面所在窗口的父级窗口,即指向 https://domain-a.com 的窗口对象。

https://domain-b.com 的页面中,我们可以使用以下代码监听消息并做出相应处理:

window.addEventListener("message", event => {
  const origin = event.origin; // 发送消息的域名
  const data = event.data; // 消息内容

  if (origin === "https://domain-a.com") {
    console.log("Received message from domain-a:", data);
  }
});

使用 postMessage 进行跨域通信需要注意安全问题,特别是在确定目标域名时应该使用固定的字符串而不是动态生成的值,以避免被攻击者利用。

iframe 是否可以使用 postMessage 通信?

不同的 iframe 和同一个页面之间也可以通过 postMessage 方法进行通信。这种情况下,通信的流程和同一页面中不同窗口的通信流程基本相同。只不过发送方和接收方不在同一页面中,而是在不同的 iframe 中。假设页面 A 中有两个 iframe,一个是 B 页面,另一个是 C 页面,现在需要在这两个 iframe 之间进行通信,具体的实现过程如下:

在 B 页面的脚本中使用 postMessage 方法向父级页面 A 发送消息:

window.parent.postMessage('message from B', 'http://localhost:3000');

在 C 页面的脚本中使用 postMessage 方法向父级页面 A 发送消息:

window.parent.postMessage('message from C', 'http://localhost:3000');

在页面 A 的脚本中监听 message 事件,接收来自不同 iframe 的消息:

window.addEventListener('message', function(event) {
  // 判断消息来源是否是指定的 iframe
  if (event.origin === 'http://localhost:3000') {
    console.log('Received message: ' + event.data);
  }
});

需要注意的是,在这个过程中,B 和 C 两个 iframe 都需要和父级页面 A 都处于同一域名下,否则会触发跨域安全限制。

网络模型分层大概有哪些层级?

计算机网络体系结构通常被划分为七层,即OSI(Open System Interconnection,开放式系统互联)参考模型TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/互联网协议)参考模型。

OSI参考模型包含七层,从底层到顶层依次是:

物理层(Physical Layer):负责将比特流传输到物理媒介上,如电缆、光纤等。

数据链路层(Data Link Layer):负责将比特流组装成帧,进行差错校验、流量控制等操作。

网络层(Network Layer):负责将数据包从源节点传输到目的节点,实现路由选择、拥塞控制等功能。

传输层(Transport Layer):负责为应用层提供可靠的端到端通信,常用的协议有TCP和UDP。

会话层(Session Layer):负责建立、管理、终止进程之间的会话连接,使不同应用程序之间能够进行数据交互。

表示层(Presentation Layer):负责对数据进行编码、解码和加密、解密,保证数据在传输过程中的安全性和正确性。

应用层(Application Layer):提供用户接口,实现用户与计算机网络之间的交互。

CP/IP参考模型包含四层,从底层到顶层依次是:

网络接口层(Network Interface Layer):负责将数据帧封装成包,并进行物理层的传输。

网络层(Internet Layer):负责将数据包从源节点传输到目的节点,实现路由选择、拥塞控制等功能。

传输层(Transport Layer):负责为应用层提供可靠的端到端通信,常用的协议有TCP和UDP。

应用层(Application Layer):提供用户接口,实现用户与计算机网络之间的交互。

需要注意的是: OSI参考模型和TCP/IP参考模型虽然不完全一致,但两者都包含了物理层、数据链路层、网络层和应用层。传输层和会话层、表示层在TCP/IP参考模型中被合并为了传输层。

DOM事件类相关问题

DOM事件级别、DOM事件模型、DOM事件流、DOM事件捕获的具体流程、Event对象的常见应用、自动以事件

dom 级别

DOM级别一共可以分为四个级别:DOM0级、DOM1级、DOM2级和DOM3级。
DOM级别其实就是标准的迭代,对于版本的称呼,类似ES5、ES6。

1、DOM0级

DOM没有被W3C定为标准之前。

2、DOM1级

1998年10月成为W3C的标准后,称为DOM1级。DOM1级由两个模块组成:DOM核心(DOM Core)和DOM HTML。其中,DOM核心规定的是如何映射基于XML的文档结构,以便简化对文档中任意部分的访问和操作。DOM HTML模块则在DOM核心的基础上加以扩展,添加了针对HTML的对象和方法。

3、DOM2级

在DOM1级的基础上进行了扩展。为节点添加了更多方法和属性等。
添加新的模块,包括:视图、事件、范围、遍历、样式等。

4、DOM3级

DOM3级进一步扩展了DOM,增加了XPath模块、加载和保存(DOM Load and Save)模块等,开始支持XML1.0规范。

DOM事件

1、DOM0级事件

DOM0级处理事件就是将一个函数赋值给一个事件处理属性。

<button id="btn" type="button"></button> 
 
var btn = document.getElementById('btn')
btn.onclick = function() { 
    console.log('Hello World')
}
// 将一个函数赋值给了一个事件处理属性onclick 这样的方法就是DOM0级。
// 可以通过给事件处理属性赋值null来解绑事件。

DOM2级事件

DOM2级处理事件是在DOM0级处理事件的基础上再添加了一些处理程序。

  • 可以同时绑定多个事件处理函数。
  • 定义了 addEventListener 和 removeEventListener 两个方法。
element.addEventListener(eventName, fn, useCapture)
// 第三个参数 useCapture:指定事件是否在捕获或冒泡阶段执行。布尔值,可选,默认false
// 可能值:true - 事件句柄在捕获阶段执行;false- 默认。事件句柄在冒泡阶段执行

<button id="btn" type="button"></button> 
 
var btn = document.getElementById('btn')
function showFn() { 
    alert('Hello World')
}
function LogFn() { 
    alert('Hello World')
}
// 同时绑定多个事件处理函数
btn.addEventListener('click', showFn);
btn.addEventListener('click', LogFn);

// 解绑事件 
btn.removeEventListener('click', showFn); 

DOM3级事件

DOM3级处理事件是在DOM2级处理事件的基础上再添加了很多事件类型。

  • UI事件,当用户与页面上的元素交互时触发,如:loadscroll
  • 焦点事件,当元素获得或失去焦点时触发,如:blurfocus
  • 鼠标事件,当用户通过鼠标在页面执行操作时触发如:dbclickmouseup
  • 滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel
  • 文本事件,当在文档中输入文本时触发,如:textInput
  • 键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydownkeypress
  • 合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart
  • 变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified

同时DOM3级事件也允许使用者自定义一些事件。

事件模型

捕获(从上到下)、冒泡(从下到上);

事件流

用户和浏览器做交互的过程中,事件的传递,比如点击左键,怎么传递到页面上的。

捕获->目标阶段->冒泡

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>事件冒泡</title>
</head>
<body>
  <div id="parent">
    我是父元素
    <span id="son">我是子元素</span>
  </div>
</body>
<script type="text/javascript">
var parent = document.getElementById('parent');
var son = document.getElementById('son');

parent.addEventListener('click', () => {
  alert('父级冒泡');
}, false);
parent.addEventListener('click', () => {
  alert('父级捕获');
}, true);
son.addEventListener('click', () => {
  alert('子级捕获');
}, true);
son.addEventListener('click', () => {
  alert('子级冒泡');
}, false);
</script>
</html>

当点击父元素:父级冒泡 -> 父级捕获
当点击子元素:父级捕获 -> 子级捕获 -> 子级冒泡 -> 父级冒泡

CORS 是如何实现跨域的?

cors 的基本概念

CORS(Cross-Origin Resource Sharing,跨域资源共享)是一种用于让浏览器绕过同源策略限制,实现跨域访问资源的机制。在浏览器端,JavaScript 的跨域请求必须要经过浏览器的同源策略限制,即只能向同一域名下的服务器发送请求,而不能向其它域名的服务器发送请求。CORS 提供了一种通过在服务端设置响应头的方式来实现浏览器端跨域请求的机制。

基本概念有哪些?

  1. 预检请求(Preflight Request):在实际请求之前,浏览器会发送一个预检请求OPTIONS,来确认服务端是否接受实际请求。

  2. 简单请求(Simple Request):满足以下条件的请求为简单请求:请求方法为GET、HEAD或POST;HTTP头信息不超出Accept、Accept-Language、Content-Language、Content-Type、Last-Event-ID、DPR、Save-Data、Viewport-Width、Width;Content-Type的值仅限于text/plain、multipart/form-data、application/x-www-form-urlencoded。

  3. 非简单请求(Non-simple Request):不满足简单请求条件的请求。

  4. CORS安全规则(CORS Safelisting Rules):指的是CORS中服务端响应的Access-Control-Allow-Origin,指定是否允许跨域请求的源。

  5. withCredentials:指的是XMLHttpRequest中的一个属性,用于在请求中携带cookie信息。

  6. 暴露Header(Exposed Headers):在CORS响应中,Access-Control-Expose-Headers头用于暴露哪些响应头给客户端使用。

  7. 存储 Cookies(Cookie Storage):跨域请求中,浏览器默认不会发送cookie信息,需要在服务端设置Access-Control-Allow-Credentials和客户端设置withCredentials为true才能实现。

  8. 跨域请求中的安全问题(CORS Security Issues):CORS的出现,引入了一些安全问题,例如CSRF、XSS等,需要在开发中做好防范措施。

如何实现跨域请求?

在 HTTP 请求中,使用了 CORS 标准头部来告诉浏览器该请求是跨域请求,并且在服务端设置 Access-Control-Allow-Origin 头部来允许指定的域名访问资源。

客户端 CORS 标准头部有以下几个:

  • Origin:表示请求来自哪个域名。
  • Access-Control-Request-Method:表示请求的方法类型(比如 GET、POST 等)。
  • Access-Control-Request-Headers:表示请求头中的额外信息(比如 Content-Type 等)。

服务端返回的响应头部有以下几个:

  • Access-Control-Allow-Origin:表示允许的域名访问该资源,可以设置为 * 表示任何域名都可以访问。
  • Access-Control-Allow-Credentials:表示是否允许浏览器携带 Cookie 和认证信息等,默认为 false。
  • Access-Control-Allow-Methods:表示允许的请求方法类型。
  • Access-Control-Allow-Headers:表示允许的请求头中的额外信息。

通过在服务端设置这些头部,可以实现跨域请求的授权和安全验证。

预检请求 作用是啥?

预检请求(Preflight Request)是CORS中的一种特殊请求,主要用于在实际请求之前,增加一次HTTP查询请求,以检查实际请求是否可以被服务器接受。

在CORS中,有些HTTP请求是简单请求(Simple Request),比如GET和POST请求,可以直接发送。而对于一些复杂请求,比如请求方法为PUT、DELETE、PATCH等,或者Content-Type类型为application/json、application/xml等,会在发送真正请求之前,增加一次HTTP查询请求,以便服务器能够知道是否允许该请求。这个查询请求就是预检请求,用来查询服务器是否支持该请求,并给出支持的条件。

预检请求中包含了一些额外的HTTP头信息,比如Origin、Access-Control-Request-Method、Access-Control-Request-Headers等,这些信息告诉服务器实际请求中会包含哪些信息,并请求服务器在实际请求中是否能够接受这些信息。

服务器接收到预检请求后,会根据请求头中的信息来判断是否允许实际请求。如果允许,会在响应头中加入一些额外的信息,比如Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers等,告诉浏览器实际请求可以被接受。如果不允许,则不会发送实际请求,而是直接返回一个错误响应。

如何避免 cors 中的一些安全问题?

在CORS中有一些安全问题,例如CSRF(跨站点请求伪造)攻击和CORS劫持。以下是避免这些问题的一些方法:

  1. CSRF攻击:使用CSRF令牌来验证请求,这样只有在正确的来源站点上发出的请求才会被视为有效请求。

  2. CORS劫持:在响应中添加Access-Control-Allow-Origin标头,并设置为信任的站点。另外,也可以使用Content-Security-Policy标头来限制JavaScript的执行。

  3. 永远不要在CORS请求中使用敏感凭据(例如Cookie和HTTP身份验证信息)。

  4. 限制跨域请求的范围,只允许特定的来源站点。

  5. 在服务器上使用防火墙和其他安全措施来保护应用程序,例如SSL / TLS加密,HTTP Strict Transport Security(HSTS)等。

总之,应该采取适当的安全措施来防止CORS相关的安全问题。

请简述 HTTP 请求的过程

HTTP(Hypertext Transfer Protocol)是一种用于传输数据的协议。当我们在浏览器中输入 URL,点击链接或提交表单时,浏览器会发送 HTTP 请求,并等待服务器的响应。以下是 HTTP 请求的基本过程:

建立连接:浏览器向服务器发出连接请求,服务器接受请求并建立连接。

发送请求:浏览器向服务器发送 HTTP 请求。请求包括请求方法(GET、POST、PUT、DELETE等)、请求头(包含一些元数据,如 Accept、Content-Type、Authorization 等)、请求体(POST 和 PUT 请求会带上数据)等。

接受请求:服务器接受请求并解析请求。服务器会根据请求的内容进行相应的处理,如查询数据库、读取文件等。

发送响应:服务器向浏览器发送 HTTP 响应。响应包括响应状态码、响应头、响应体等。常见的响应状态码包括 200 OK、404 Not Found、500 Internal Server Error 等。

接受响应:浏览器接受响应并解析响应。浏览器会根据响应的内容进行相应的处理,如渲染页面、执行 JavaScript 等。

断开连接:请求处理完毕后,浏览器和服务器会断开连接。

需要注意的是,HTTP 是一种无状态协议,即每次请求都是独立的,服务器不会保留任何关于请求的信息。为了保持客户端与服务器之间的状态,通常使用 Cookie 或 Session 等机制来保存状态信息。

此外,现代浏览器通常会使用 HTTP 缓存来提高性能。当浏览器发送请求时,如果发现资源已经在本地缓存中存在,就会直接使用缓存的版本,而不是重新从服务器下载。可以使用 Cache-Control、Expires 等响应头控制缓存的行为。

JS 中继承方式有哪些?

1、借助构造函数实现继承

call和apply改变的是JS运行的上下文:

/*借助构造函数实现继承*/
function Parent(name) {
    this.name = name;
    this.getName = function () {
        console.log(this.name);
    }
}

function Child(name) {
    Parent.call(this, name);
    this.type = 'child1'
}

let child = new Child('yanle');
child.getName();
console.log(child.type);

父类的this指向到了子类上面去,改变了实例化的this 指向,导致了父类执行的属性和方法,都会挂在到 子类实例上去;
缺点:父类原型链上的东西并没有被继承;

2、通过原型链实现继承

/*通过原型链实现继承*/
function Parent2(){
    this.name='parent2'
}

function Child2(){
    this.type='child2'
}

Child2.prototype=new Parent2();
console.log(new Child2());

Child2.prototype是Child2构造函数的一个属性,这个时候prototype被赋值了parent2的一个实例,实例化了新的对象Child2()的时候,
会有一个__proto__属性,这个属性就等于起构造函数的原型对象,但是原型对象被赋值为了parent2的一个实例,
所以new Child2的原型链就会一直向上找parent2的原型

var s1=new Child2();
var s2=new Child2();
s1.proto===s2.proto;//返回true

缺点:通过子类构造函数实例化了两个对象,当一个实例对象改变其构造函数的属性的时候,
那么另外一个实例对象上的属性也会跟着改变(期望的是两个对象是隔离的赛);原因是构造函数的原型对象是公用的;

3、组合方式

/*组合方式*/
function Parent3(){
    this.name='parent3';
    this.arr=[1,2,3];
}

function Child3(){
    Parent3.call(this);
    this.type='child';
}

Child3.prototype=new Parent3();
var s3=new Child3();
var s4=new Child3();
s3.arr.push(4);
console.log(s3,s4);

**优点:**这是最通用的使用方法,集合了上面构造函数继承,原型链继承两种的优点。
**缺点:**父类的构造函数执行了2次,这是没有必要的,
constructor指向了parent了

4、组合继承的优化

/*组合继承的优化1*/
function Parent4(){
    this.name='parent3';
    this.arr=[1,2,3];
}

function Child4(){
    Parent4.call(this);
    this.type='child5';
}

Child4.prototype=Parent4.prototype;
var s5=new Child4();
var s6=new Child4()

**缺点:**s5 instaceof child4 //true, s5 instanceof Parent4//true
我们无法区分一个实例对象是由其构造函数实例化,还是又其构造函数的父类实例化的
s5.constructor 指向的是Parent4;//原因是子类原型对象的constructor 被赋值为了父类原型对象的 constructor,所以我们使用constructor的时候,肯定是指向父类的
Child3.constructor 也有这种情况

5、组合继承的优化2

function Parent5() {
    this.name = 'parent5';
    this.play = [1, 2, 3];
}

function Child5() {
    Parent5.call(this);
    this.type = 'child5'
}

Child5.prototype = Object.create(Parent5.prototype);
//这个时候虽然隔离了,但是constructor还是只想的Parent5的,因为constructor会一直向上找
Child5.prototype.constructor=Child5;

var s7=new Child5();
console.log(s7 instanceof Child5,s7 instanceof Parent5);
console.log(s7.constructor);

通过Object.create来创建原型中间对象,那么这么来的话,chiild5的对象prototype获得的是parent5 父类的原型对象;
Object.create创建的对象,原型对象就是参数;

6、ES 中的继承

Class 可以通过extends关键字实现继承,让子类继承父类的属性和方法。extends 的写法比 ES5 的原型链继承,要清晰和方便很多。

class Point { /* ... */ }

class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
  }

  toString() {
    return this.color + ' ' + super.toString(); // 调用父类的toString()
  }
}

详细讲一下 Symbol 数据类型特征与实际使用案例?

Symbol 概要简介

Symbol 是 ECMAScript 6 引入的一种新的原始数据类型,用来表示独一无二的值。每个 Symbol 值都是唯一的,因此可以用来创建一些独特的标识符。

定义

Symbol 的定义非常简单,只需要调用 Symbol() 方法即可,例如:

const mySymbol = Symbol();

在使用 Symbol 的时候,可以给它传递一个可选的描述信息,用于标识 Symbol 的含义,例如:

const mySymbol = Symbol('my symbol');

使用场景

常量的定义

由于每个 Symbol 的值都是唯一的,因此可以用它来定义常量,避免不小心修改值。例如:

const MY_CONST = Symbol('my_const')

Symbol 值可以作为对象的属性名,用来避免属性名冲突

例如:

const obj = {};
const mySymbol = Symbol('my symbol');
obj[mySymbol] = 'hello';
console.log(obj[mySymbol]); // 输出:'hello'

在使用 Symbol 的时候,可以结合 Object.defineProperty() 方法来定义一个只读的属性

例如:

const obj = {};
const mySymbol = Symbol('my symbol');
Object.defineProperty(obj, mySymbol, {
    value: 'hello',
    writable: false
});
console.log(obj[mySymbol]); // 输出:'hello'
obj[mySymbol] = 'world';
console.log(obj[mySymbol]); // 输出:'hello'

还可以使用 Symbol.for() 方法创建一个可共享的 Symbol 值

例如:

const s1 = Symbol.for('foo');
const s2 = Symbol.for('foo');
console.log(s1 === s2); // 输出:true

在上述示例中,虽然 s1 和 s2 的值不同,但是它们所表示的含义相同,因此可以认为它们是相等的。这种通过 Symbol.for() 方法创建的 Symbol 值,会被保存在一个全局的 Symbol 注册表中,可以被不同的代码块访问到。

私有属性的定义

由于 Symbol 值是唯一的,因此可以用它来模拟私有属性的概念,防止属性名冲突。例如:

const _myPrivateProp = Symbol('my_private_prop')
class MyClass {
    constructor() {
    this[_myPrivateProp] = 'private value'
}
getPrivateValue() {
    return this[_myPrivateProp]
    }
}

在这个例子中,_myPrivateProp 就是一个 Symbol 值,用于存储私有属性的值,它无法被外部访问到,只能通过类的方法来获取它的值。

自定义迭代器

Symbol 还可以用于自定义迭代器,例如:

const myIterable = {
  [Symbol.iterator]: function* () {
    yield 1
    yield 2
    yield 3
  }
}
for (let value of myIterable) {
  console.log(value)
}
// Output: 1 2 3

在这个例子中,我们使用了 Symbol.iterator 来定义了一个自定义的迭代器,这个迭代器可以被 for...of 循环调用来遍历对象的属性值。

总结

总之,Symbol 的主要用途是创建独一无二的属性名,用来避免属性名冲突。
在实际开发中,可以将 Symbol 作为对象的属性名来定义一些特殊的行为,例如迭代器、生成器等,这些都是 Symbol 的实际使用案例。

TCP 传输过程?

传输过程

TCP(传输控制协议)是一种面向连接的协议,它保证了数据的可靠传输。在 TCP 传输数据时,数据会被分割成一个个的数据包进行传输,具体传输过程如下:

  • 建立连接:TCP 通过三次握手建立连接,即客户端向服务器发送 SYN(同步)数据包,服务器接收到 SYN 后回应一个 SYN-ACK(同步-确认)数据包,客户端再回应一个 ACK(确认)数据包,连接建立成功。

  • 数据传输:数据在应用层被拆分成数据段,在传输层被拆分成数据包(也称为报文段),每个数据包包含源端口号、目标端口号、序列号、确认号、标志位等信息。发送方发送数据包后,等待接收方回复确认信息,如果未收到确认信息,则进行重传,直到接收方成功接收数据包。

  • 拥塞控制:当网络拥塞时,TCP 会采取措施来减少数据的传输速率,如减小窗口大小、降低拥塞窗口等。

  • 连接终止:TCP 通过四次挥手来关闭连接,即客户端发送一个 FIN(终止)数据包,服务器回应一个 ACK 数据包表示接收到 FIN,然后服务器再发送一个 FIN 数据包,客户端回应一个 ACK 数据包表示接收到 FIN,连接关闭成功。

总的来说,TCP 通过三次握手建立连接、数据分段传输、拥塞控制和四次挥手关闭连接来保证数据的可靠传输

详细说一下 TCP 通过三次握手建立连接?

TCP通过三次握手建立连接的过程如下:

  • 第一次握手:客户端向服务端发送 SYN(同步)包,其中 SYN=1,seq=x,表示客户端希望与服务端建立连接,同时指定自己的初始序号为x。此时客户端处于 SYN_SENT 状态。

  • 第二次握手:服务端接收到 SYN 包后,向客户端发送 SYN-ACK 包,其中 SYN=1,ACK=1,ack=x+1,seq=y,表示服务端已经收到客户端的请求,同意建立连接,同时指定自己的初始序号为y,确认号为x+1。此时服务端处于 SYN_RCVD 状态。

  • 第三次握手:客户端收到 SYN-ACK 包后,向服务端发送 ACK 包,其中 SYN=0,ACK=1,ack=y+1,seq=x+1,表示客户端已经收到服务端的确认,连接建立成功。此时客户端处于 ESTABLISHED 状态,服务端也处于 ESTABLISHED 状态。

这样就完成了三次握手建立连接的过程。在这个过程中,客户端和服务端都可以向对方发送数据。
需要注意的是,如果客户端在等待服务端的 SYN-ACK 包时超时或未收到响应,会重新发送 SYN 包,直到建立连接或达到最大重试次数。同时,在建立连接后,每个数据包都会在传输时带有序号和确认号,以保证数据的可靠传输。

TCP数据分段传输的过程是怎么样的?

  • 应用层将需要传输的数据分成适当大小的数据段,每个数据段称为一个TCP数据包。

  • TCP协议根据MSS(最大报文长度)将TCP数据包分割成更小的IP数据包,以适应底层网络的MTU(最大传输单元)。

  • 在传输数据之前,TCP在每个数据包中添加一个包头(header),其中包含序列号(sequence number)和确认号
    (acknowledgment number)等信息。

  • 发送方将数据包发送到网络,并等待接收方的确认响应。如果发送方没有收到确认响应,它会重新发送数据包。

  • 接收方收到数据包后,会对数据包进行确认,向发送方发送确认响应。如果接收方没有收到正确的数据包,它会要求发送方重发数据。

  • 发送方收到确认响应后,会将下一个数据包发送到网络,并等待接收方的确认响应。如果发送方没有收到确认响应,它会重新发送数据包。

  • 接收方根据收到的数据包的序列号和确认号,组装数据包,然后将数据包传递给上层应用程序。

TCP数据分段传输可以提高网络的可靠性和稳定性,避免了数据包的丢失和重传,但是也会造成额外的网络开销。

TCP 是如何进行拥塞控制?

TCP使用拥塞控制算法来避免网络中的拥塞现象,并在发生拥塞时减少发送数据的速率,从而避免网络拥塞的加剧。TCP的拥塞控制算法主要包括以下几个方面:

  • 慢启动(Slow Start):在TCP连接刚建立时,发送方限制了自己的初始发送窗口大小,从而避免发送过多的数据导致网络拥塞。发送方以指数级别的方式增加其发送窗口大小,直到达到一个阈值,然后就会进入拥塞避免状态。

  • 拥塞避免(Congestion Avoidance):在拥塞避免状态下,TCP发送方每经过一轮的传输,就将其发送窗口大小增加一个MSS(最大报文段长度)的值。这样可以逐渐增加发送窗口大小,从而提高数据传输速率。

拥塞检测(Congestion Detection):当TCP发送方收到一个超时重传的确认消息时,它就认为网络中出现了拥塞,并将其发送窗口大小减半,然后重新进入慢启动状态。

  • 拥塞避免(Congestion Avoidance):当TCP发送方收到一个失序的确认消息时,它就知道它发送的某些数据包在网络中已经丢失了,此时就不必等到超时重传定时器时间到期,而是立即重传那些丢失的数据包。

  • 快速恢复(Fast Recovery):在快速重传后,TCP发送方将进入快速恢复状态,其中发送方的发送窗口大小将设置为丢失数据包的数量加上MSS的值,从而避免了发送窗口大小的降低和慢启动状态的重新启动。

四次挥手关闭连接流程如何?

TCP 通过四次挥手来关闭连接,具体过程如下:

  • 客户端向服务端发送 FIN 报文,表示客户端不再发送数据。

  • 服务端收到 FIN 报文后,向客户端发送 ACK 报文,表示收到了客户端的 FIN 报文。

  • 服务端向客户端发送 FIN 报文,表示服务端不再发送数据。

  • 客户端收到 FIN 报文后,向服务端发送 ACK 报文,表示收到了服务端的 FIN 报文。

图示如下:

客户端                服务端
|                      |
|  FIN(seq=x)          |
|--------------------->|
|  ACK(seq=x+1,ack=y)  |
|<---------------------|
|                      |
|  FIN(seq=y)          |
|<---------------------|
|  ACK(seq=y+1,ack=x+1)|
|--------------------->|

其中,seq 表示序号,ack 表示确认号。第一次握手中,客户端发送的序号 seq=x,表示客户端的数据流的第一个字节的序号。第二次握手中,服务端发送的确认号 ack=y,表示服务端期望下一个收到的字节的序号是 y。第三次握手中,服务端发送的序号 seq=y,表示服务端的数据流的第一个字节的序号。第四次握手中,客户端发送的确认号 ack=x+1,表示客户端期望下一个收到的字节的序号是 x+1。注意,在第四次握手中,客户端发送 ACK 报文后,不再发送数据,但服务端可能还有数据需要发送,因此服务端需要先发送 FIN 报文。

四次挥手的过程中,最后一个 ACK 报文可能会丢失,因此需要等待一段时间后才能确认连接已经关闭。这个等待时间称为 TIME_WAIT 状态,一般为 2MSL(Maximum Segment Lifetime,最长报文寿命)时间,即一个报文在网络中最长的生命周期,通常为 2 分钟。

值得注意的是,TCP 的四次挥手过程是可靠的,可以确保数据可靠传输。但由于四次挥手需要消耗额外的时间和网络资源,因此在某些情况下,可以使用 TCP 的强制断开连接方式(RST),通过发送一个 RST 报文来立即中断连接。但这种方式可能会导致数据的丢失和损坏,因此应该谨慎使用。

Proxy 和 Reflect 了解多少?

Proxy

基本概念

Proxy(代理) 是 ES6 中新增的一个特性。Proxy 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式。
使用 Proxy 的好处是:对象只需关注于核心逻辑,一些非核心的逻辑 (如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)可以让 Proxy 来做。 从而达到关注点分离,降级对象复杂度的目的。

api 有哪些?

var p = new Proxy(target, handler);
其中,target 为被代理对象。handler 是一个对象,其声明了代理 target 的一些操作。p 是代理后的对象。当外界每次对 p 进行操作时,就会执行 handler 对象上的一些方法。

handler 能代理的一些常用的方法如下:

  • handler.getPrototypeOf(): Object.getPrototypeOf 方法的捕捉器。

  • handler.setPrototypeOf(): Object.setPrototypeOf 方法的捕捉器。

  • handler.isExtensible(): Object.isExtensible 方法的捕捉器。

  • handler.preventExtensions(): Object.preventExtensions 方法的捕捉器。

  • handler.getOwnPropertyDescriptor(): Object.getOwnPropertyDescriptor 方法的捕捉器。

  • handler.defineProperty(): Object.defineProperty 方法的捕捉器。

  • handler.has(): in 操作符的捕捉器。

  • handler.get(): 属性读取操作的捕捉器。

  • handler.set(): 属性设置操作的捕捉器。

  • handler.deleteProperty(): delete 操作符的捕捉器。

  • handler.ownKeys(): Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。

  • handler.apply(): 函数调用操作的捕捉器。

  • handler.construct(): new 操作符的捕捉器。
    ...

基础使用

var target = {
   name: 'obj'
 };
 var logHandler = {
   get: function(target, key) {
     console.log(`${key} 被读取`);
     return target[key];
   },
   set: function(target, key, value) {
     console.log(`${key} 被设置为 ${value}`);
     target[key] = value;
   }
 };
var targetWithLog = new Proxy(target, logHandler);
targetWithLog.name;             // 控制台输出:name 被读取
targetWithLog.name = 'others';  // 控制台输出:name 被设置为 others
console.log(target.name);       // 控制台输出: others

使用示例 - 实现虚拟属性

var person = {
  fisrsName: '张',
  lastName: '小白'
};
var proxyedPerson = new Proxy(person, {
  get: function (target, key) {
    if(key === 'fullName'){
      return [target.fisrsName, target.lastName].join(' ');
    }
    return target[key];
  },
  set: function (target, key, value) {
    if(key === 'fullName'){
      var fullNameInfo = value.split(' ');
      target.fisrsName = fullNameInfo[0];
      target.lastName = fullNameInfo[1];
    } else {
      target[key] = value;
    }
  }
});

console.log('姓:%s, 名:%s, 全名: %s', proxyedPerson.fisrsName, proxyedPerson.lastName, proxyedPerson.fullName);// 姓:张, 名:小白, 全名: 张 小白
proxyedPerson.fullName = '李 小露';
console.log('姓:%s, 名:%s, 全名: %s', proxyedPerson.fisrsName, proxyedPerson.lastName, proxyedPerson.fullName);// 姓:李, 名:小露, 全名: 李 小露

使用示例 - 实现私有变量

下面的 demo 实现了真正的私有变量。代理中把以 _ 开头的变量都认为是私有的。

var api = {
  _secret: 'xxxx',
  _otherSec: 'bbb',
  ver: 'v0.0.1'
};

api = new Proxy(api, {
  get: function(target, key) {
    // 以 _ 下划线开头的都认为是 私有的
    if (key.startsWith('_')) {
      console.log('私有变量不能被访问');
      return false;
    }
    return target[key];
  },
  set: function(target, key, value) {
    if (key.startsWith('_')) {
      console.log('私有变量不能被修改');
      return false;
    }
    target[key] = value;
  },
  has: function(target, key) {
    return key.startsWith('_') ? false : (key in target);
  }
});

api._secret; // 私有变量不能被访问
console.log(api.ver); // v0.0.1
api._otherSec = 3; // 私有变量不能被修改
console.log('_secret' in api); //false
console.log('ver' in api); //true

使用示例 - 抽离校验模块

function Animal() {
  return createValidator(this, animalValidator);
}
var animalValidator = {
  name: function(name) {
    // 动物的名字必须是字符串类型的
    return typeof name === 'string';
  }
};

function createValidator(target, validator) {
  return new Proxy(target, {
    set: function(target, key, value) {
      if (validator[key]) {
        // 符合验证条件
        if (validator[key](value)) {
          target[key] = value;
        } else {
          throw Error(`Cannot set ${key} to ${value}. Invalid.`);
        }
      } else {
        target[key] = value
      }
    }
  });
}

var dog = new Animal();
dog.name = 'dog';
console.log(dog.name);
dog.name = 123; // Uncaught Error: Cannot set name to 123. Invalid.

Reflect

概念

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与 proxy handler 的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。

与大多数全局对象不同 Reflect 并非一个构造函数,所以不能通过 new 运算符对其进行调用,或者将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是静态的(就像 Math 对象)。

api 有哪些?

  • Reflect.apply(target, thisArgument, argumentsList): 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。

    • 举例
    const ages = [11, 33, 12, 54, 18, 96];
    
    // 旧写法
    const youngest = Math.min.apply(Math, ages);
    const oldest = Math.max.apply(Math, ages);
    const type = Object.prototype.toString.call(youngest);
    
    // 新写法
    const youngest = Reflect.apply(Math.min, Math, ages);
    const oldest = Reflect.apply(Math.max, Math, ages);
    const type = Reflect.apply(Object.prototype.toString, youngest, []);
  • Reflect.construct(target, argumentsList[, newTarget]): 对构造函数进行 new 操作,相当于执行 new target(...args)。

    • Reflect.construct方法等同于new target(...args),这提供了一种不使用new,来调用构造函数的方法。
    function Greeting(name) {
      this.name = name;
    }
    
    // new 的写法
    const instance = new Greeting('张三');
    
    // Reflect.construct 的写法
    const instance = Reflect.construct(Greeting, ['张三']);
  • Reflect.defineProperty(target, propertyKey, attributes): 和 Object.defineProperty() 类似。如果设置成功就会返回 true

  • Reflect.deleteProperty(target, propertyKey): 作为函数的delete操作符,相当于执行 delete target[name]。该方法返回一个布尔值。

    • Reflect.deleteProperty方法等同于delete obj[name],用于删除对象属性。
    const myObj = { foo: 'bar' };
    
    // 旧写法
    delete myObj.foo;
    
    // 新写法
    Reflect.deleteProperty(myObj, 'foo');

    该方法返回一个布尔值。如果删除成功或删除的属性不存在,则返回true,如果删除失败,删除的属性依然还在,则返回false。

  • Reflect.get(target, propertyKey[, receiver]): 获取对象身上某个属性的值,类似于 target[name]。

    • Reflect.get方法查找并返回target的name属性,如果没有,则返回undefined。
    var myObject = {
      foo: 1,
      bar: 2,
      get baz() {
        return this.foo + this.bar;
      },
    }
    
    Reflect.get(myObject, 'foo') // 1
    Reflect.get(myObject, 'bar') // 2
    Reflect.get(myObject, 'baz') // 3
    • 读取函数的this绑定的receiver
    var myObject = {
      foo: 1,
      bar: 2,
      get baz() {
        return this.foo + this.bar;
      },
    };
    
    var myReceiverObject = {
      foo: 4,
      bar: 4,
    };
    
    Reflect.get(myObject, 'baz', myReceiverObject) // 8
    • 如果第一个参数不是对象,则Reflect.get则会报错。
  • Reflect.getOwnPropertyDescriptor(target, propertyKey): 类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符,否则返回 undefined。

  • Reflect.getPrototypeOf(target): 类似于 Object.getPrototypeOf()。

  • Reflect.has(target, propertyKey): 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。

    • Reflect.has对应 name in obj 里面的in操作
    var myObject = {
      foo: 1,
    };
    
    // 旧写法
    'foo' in myObject // true
    
    // 新写法
    Reflect.has(myObject, 'foo') // true

    如果第一个参数不是对象,Reflect.has和in都会报错。

  • Reflect.isExtensible(target): 类似于 Object.isExtensible().

  • Reflect.ownKeys(target): 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable 影响).

  • Reflect.preventExtensions(target): 类似于 Object.preventExtensions()。返回一个Boolean。

  • Reflect.set(target, propertyKey, value[, receiver]): 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。

    • Reflect.set方法设置target对象的name属性等于value。
    var myObject = {
        foo: 1,
        set bar(value) {
          return this.foo = value;
        },
    }
    
    myObject.foo // 1
    
    Reflect.set(myObject, 'foo', 2);
    myObject.foo // 2
    
    Reflect.set(myObject, 'bar', 3)
    myObject.foo // 3
    • 如果name属性设置的赋值函数,则赋值函数的this绑定receiver。
    var myObject = {
        foo: 4,
        set bar(value) {
          return this.foo = value;
        },
    };
    
    var myReceiverObject = {
      foo: 0,
    };
    
    Reflect.set(myObject, 'bar', 1, myReceiverObject);
    myObject.foo // 4
    myReceiverObject.foo // 1
  • Reflect.setPrototypeOf(target, prototype): 设置对象原型的函数。返回一个 Boolean,如果更新成功,则返回 true。

Promise 了解多少?

Promise 对象

1、Promise 的含义

Promise 是异步编程的一种解决方案,比传统的解决方案 —— 回调函数和事件 —— 更合理和更强大。它由社区最早提出和实现, ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

Promise对象有以下两个特点。

( 1 )对象的状态不受外界影响。
Promise对象代表一个异步操作,有三种状态:Pending(进行中)、Resolved(已完成,又称 Fulfilled )和Rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是 “ 承诺 ” ,表示其他手段无法改变。

( 2 )一旦状态改变,就不会再变,任何时候都可以得到这个结果。
Promise对象的状态改变,只有两种可能:从Pending变为Resolved和从Pending变为Rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件( Event )完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。
Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

2、基本用法

实例1:基本用法

    var promise = new Promise(function(resolve, reject) {
    	// ... some code
        if (/*  异步操作成功 */){
            resolve(value);
        } else {
            reject(error);
        }
    });

实例2:只要一new Promise后就会立即执行。

    let promise = new Promise(function(resolve, reject) {
        console.log('Promise');
        resolve();
    });
    promise.then(function() {
        console.log('Resolved.');
    });
    console.log('Hi!');
    // Promise
    // Hi!
    // Resolved

实例3:下面是一个用 Promise 对象实现的 Ajax 操作的例子。(非常经典)

    var getJSON = function (url) {
        var promise = new Promise(function (resolve, reject) {
            var client = new XMLHttpRequest();
            client.open("GET", url);
            client.onreadystatechange = handler;
            client.responseType = "json";
            client.setRequestHeader("Accept", "application/json");
            client.send();
    
            function handler() {
                if (this.readyState !== 4) {
                    return;
                }
                if (this.status === 200) {
                    resolve(this.response);
                } else {
                    reject(new Error(this.statusText));
                }
            };
        });
        return promise;
    };
    
    getJSON("/posts.json").then(function (json) {
        console.log('Contents: ' + json);
    }, function (error) {
        console.error(' 出错了 ', error);
    });

3、Promise.prototype.then()

Promise 实例具有then方法,也就是说,then方法是定义在原型对象 Promise.prototype 上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是 Resolved 状态的回调函数,第二个参数(可选)是 Rejected 状态的回调函数。
then方法返回的是一个新的 Promise 实例(注意,不是原来那个 Promise 实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

    getJSON("/posts.json").then(function(json) {
        return json.post;
    }).then(function(post) {
        // ...
    });

采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个 Promise 对象(即有异步操作),这时后一个回调函数,就会等待该 Promise 对象的状态发生变化,才会被调用。

    getJSON("/post/1.json").then(function (post) {
        return getJSON(post.commentURL);
    }).then(function funcA(comments) {
        console.log("Resolved: ", comments);
    }, function funcB(err) {
        console.log("Rejected: ", err);
    });

上面代码中,第一个then方法指定的回调函数,返回的是另一个 Promise 对象。这时,第二个then方法指定的回调函数,就会等待这个新的 Promise 对象状态发生变化。如果变为 Resolved ,就调用funcA,如果状态变为 Rejected ,就调用funcB。

4、Promise.prototype.catch()

Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。
实例:

    getJSON("/posts.json").then(function (posts) {
        // ...
    }).catch(function (error) {
        //  处理 getJSON  和 前一个回调函数运行时发生的错误
        console.log(' 发生错误! ', error);
    });

跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数, Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

    var someAsyncThing = function () {
        return new Promise(function (resolve, reject) {
        //  下面一行会报错,因为 x 没有声明
            resolve(x + 2);
        });
    };
    someAsyncThing().then(function () {
        console.log('everything is great');
    });

5、Promise.all()

Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
var p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all方法接受一个数组作为参数,p1、p2、p3都是 Promise 对象的实例。

实例:

    Promise.all([checkLogin(),getUserInfo()]).then(([res1,res2])=>{
        console.log(`result1:${res1.result}, result2:${res2.userID}`)
    });

6、Promise.race()

Promise.race(iterable) 方法返回一个 promise,一旦迭代器中的某个 promise 解决或拒绝,返回的 promise 就会解决或拒绝。

7、Promise.resolve()

有时需要将现有对象转为 Promise 对象,Promise.resolve方法就起到这个作用。

    var jsPromise = Promise.resolve($.ajax('/whatever.json'));  

8、Promise.reject()

Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected。它的参数用法与Promise.resolve方法完全一致。

9、两个有用的附加方法

ES6 的 Promise API 提供的方法不是很多,有些有用的方法可以自己部署。下面介绍如何部署两个不在 ES6 之中、但很有用的方法。

9.1、done()

Promise 对象的回调链,不管以then方法或catch方法结尾,要是最后一个方法抛出错误,都有可能无法捕捉到(因为 Promise 内部的错误不会冒泡到全局)。因此,我们可以提供一个done方法,总是处于回调链的尾端,保证抛出任何可能出现的错误。

    asyncFunc()
        .then(f1)
        .catch(r1)
        .then(f2)
        .done();

9.2、finally()

finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。它与done方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。

    server.listen(0)
        .then(function () {
    // run test
        })
        .finally(server.stop);

10、Promise的使用

使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个Promise对象。

    function getFoo() {
        return new Promise(function (resolve, reject) {
            resolve('foo');
        });
    }
    
    var g = function* () {
        try {
            var foo = yield getFoo();
            console.log(foo);
        } catch (e) {
            console.log(e);
        }
    };
    
    function run(generator) {
        var it = generator();
    
        function go(result) {
            if (result.done) return result.value;
            return result.value.then(function (value) {
                return go(it.next(value));
            }, function (error) {
                return go(it.throw(error));
            });
        }
    
        go(it.next());
    }
    
    run(g);

参考文章

其他Promise相关文档

什么是同源策略?

同源策略是一种安全机制,它是浏览器对 JavaScript 实施的一种安全限制。所谓“同源”是指域名、协议、端口号均相同。同源策略限制了一个页面中的脚本只能与同源页面的脚本进行交互,而不能与不同源页面的脚本进行交互。这是为了防止恶意脚本窃取数据、进行 XSS 攻击等安全问题。

同源策略限制的资源包括:

  • Cookie、LocalStorage 和 IndexDB 等存储性资源
  • AJAX、WebSocket 等发送 HTTP 请求的方法
  • DOM 节点
  • 其他通过脚本或插件执行的跨域请求

这些资源只能与同源页面进行交互,不能与不同源的页面进行交互。

WebSocket 了解多少?

基本概念

WebSocket 是一种基于 TCP 协议的网络通信协议,可以在客户端和服务器之间进行双向通信。相比传统的 HTTP 请求,WebSocket 具有更低的延迟和更高的效率。但是,由于同源策略的限制,WebSocket 也会受到跨域问题的影响。

WebSocket 通过在客户端和服务器之间建立持久连接来解决跨域问题。WebSocket 的握手过程与 HTTP 协议相似,客户端首先通过 HTTP 请求与服务器建立连接,然后服务器返回一个握手响应,表示连接已经建立成功。在握手完成后,客户端和服务器之间就可以通过该连接进行双向通信,不受同源策略的限制。

需要注意的是,WebSocket 协议本身并没有限制跨域请求,但是在实际使用中,服务器通常会限制 WebSocket 连接的来源。这是出于安全性考虑,避免恶意网站通过 WebSocket 连接获取敏感信息。因此,在使用 WebSocket 进行跨域通信时,需要确保服务器允许来自指定域名或 IP 地址的连接。

WebSocket 同源限制是啥?

WebSocket 通信协议本身不受同源策略限制,因为 WebSocket 是一个独立的协议。但是在建立 WebSocket 连接时,需要进行握手,握手时会发送 HTTP 请求头,因此受到同源策略的限制。需要满足以下条件才能建立 WebSocket 连接:

  • 协议头必须为 "ws://" 或 "wss://"(安全的 WebSocket)

  • 域名和端口必须与当前网页完全一致

如果以上条件不满足,浏览器将不允许建立 WebSocket 连接。

关于请求头的问题

在建立WebSocket连接时,需要添加Upgrade头和Connection头,其中Upgrade头指明这是一个WebSocket请求,Connection头指明连接方式为升级连接(upgrade)。
服务器如果同意升级,则会返回 101 状态码,表示升级成功,此时 WebSocket 连接建立成功,双方就可以通过该连接进行双向通信。这个过程与同源策略无关,因此 WebSocket 不会受到同源策略的限制。

new WebSocket(url) 创建 WebSocket 对象时,会自动添加 Upgrade 头和 Connection 头。这是因为在 WebSocket 协议中,这两个头部是必需的,用于告知服务器客户端希望建立 WebSocket 连接。
示例代码如下:

const socket = new WebSocket('ws://localhost:8080');

此外,在WebSocket请求中也可以添加一些自定义的请求头,例如:

const socket = new WebSocket('ws://localhost:8080', {
  headers: {
    'X-Custom-Header': 'hello',
    'Y-Custom-Header': 'world'
  }
});

建立一个 WebSocket 连接需要以下步骤:

1. 创建一个 WebSocket 对象

const socket = new WebSocket('ws://localhost:8080');

2. 监听 WebSocket 事件
WebSocket 对象是一个 EventTarget 对象,它可以监听多个事件。常见的事件有 open、message、error 和 close。

  • open 事件:WebSocket 连接建立成功时触发。
socket.addEventListener('open', event => {
  console.log('WebSocket 连接已建立');
});
  • message 事件:WebSocket 收到消息时触发。
socket.addEventListener('message', event => {
  console.log(`收到消息:${event.data}`);
});
  • error 事件:WebSocket 连接出错时触发。
socket.addEventListener('error', event => {
  console.error('WebSocket 连接出错', event);
});
  • close 事件:WebSocket 连接关闭时触发。
socket.addEventListener('close', event => {
  console.log('WebSocket 连接已关闭');
});

以上是建立 WebSocket 连接的基本步骤。需要注意的是,在使用 WebSocket 协议时,服务器端也需要提供相应的支持。

服务端支持

要支持 WebSocket,服务器需要在接收到客户端 WebSocket 握手请求时,返回符合 WebSocket 协议规范的响应。在 Node.js 中,我们可以使用 ws 模块来实现 WebSocket 服务器。以下是一个简单的 WebSocket 服务器的示例代码:

const WebSocket = require('ws');

const server = new WebSocket.Server({ port: 8080 });

server.on('connection', (socket) => {
  console.log('Client connected');

  socket.on('message', (message) => {
    console.log(`Received message: ${message}`);

    // Echo the message back to the client
    socket.send(`Echo: ${message}`);
  });

  socket.on('close', () => {
    console.log('Client disconnected');
  });
});

需要注意的是,在生产环境中,我们需要使用 HTTPS 协议来保证 WebSocket 的安全性。同时,我们还需要注意处理异常情况,例如客户端断开连接等。

其中 ws 不是 Node.js 的内置模块,它是一个第三方模块,可以使用 npm 安装。在 Node.js 应用中使用 WebSocket 时,需要先安装 ws 模块。可以使用以下命令进行安装:

npm install ws

服务端如何限制链接源?

在 WebSocket 建立连接的时候,可以通过检查请求的 Origin 头部信息来限制访问源。下面是一个简单的 Node.js 示例代码:

const WebSocket = require('ws');
const server = new WebSocket.Server({ port: 8080 });

server.on('connection', (ws, req) => {
  const { origin } = req.headers;
  // 判断请求的 origin 是否允许连接
  if (origin === 'https://www.example.com') {
    // 允许连接
    console.log('Connection allowed from', origin);
    ws.send('Connection allowed');
  } else {
    // 拒绝连接
    console.log('Connection refused from', origin);
    ws.close();
  }
});

"ws://" 与 "wss://" 有啥区别?

"ws://" 和 "wss://" 都是 WebSocket 协议的 URL 前缀,它们之间的区别在于传输层协议的不同。

"ws://" 使用的是普通的 HTTP 协议作为传输层协议,在传输过程中数据是明文的,容易被中间人攻击篡改数据,存在安全风险。

"wss://" 使用的是加密的 SSL/TLS 协议作为传输层协议,在传输过程中数据是加密的,更加安全。但是因为要进行 SSL/TLS 握手等复杂过程,所以 "wss://" 的连接建立时间和数据传输速度会比 "ws://" 慢一些。

因此,如果数据传输需要保密性,建议使用 "wss://",否则可以使用 "ws://"。

Http协议基础

http 协议有什么特点?

简单快速,灵活、无连接、无状态

每一个资源对应一个URI,请求只要输入资源地址uri就可以了;
在每一个http头部协议中都有一个数据类型,通过一个http协议就可以完成不同类型数据的传输;
链接一次就会断开;
每一次链接不会记住链接状态的,服务器不区分两次链接的身份;

http报文组成部分?

请求报文

请求报文:请求行、请求头、空行、请求体
请求行:HTTP请求方法、页面地址、协议版本等
请求头:key,value值,告诉服务端我要什么内容、要什么数据类型
空行:分割请求头和请求体的,遇到空行,服务器就知道,请求头结束了,接下来是请求体了
请求体:就是给服务端的一些入参数据;
我所了解的请求体有两种格式,Content-Type: application/x-www-form-urlencoded 和 payload 和 json

相应报文

状态行、响应头、空行、响应体

状态行:协议版本 状态码 状态
其他的一样的

通信协议?

建立在 TCP 之上的

常见请求头数据和相应头数据(以github某请求为例子)

Request Headers

Accept: */*                         // 告诉服务器,客户机支持的数据类型
Accept-Encoding: gzip, deflate, br  // 告诉服务器,客户机支持的数据压缩格式
Accept-Language: zh-CN,zh;q=0.9     // 编码格式
Connection: keep-alive              // 是否支持场链接
Content-Length: 12308               // 获取文件的总大小
Content-Type: application/json      // 返回数据格式
Host: api.github.com
Origin: https://github.com
Referer: https://github.com/yanlele/node-index/blob/master/book/05%E3%80%81%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%E7%82%B9%E4%B8%93%E9%A2%98/01_01%E3%80%81%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%E9%83%A8%E5%88%861-10.md
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36

Response Headers:

Access-Control-Allow-Origin: *                          // 允许跨域策略
Access-Control-Expose-Headers: ETag, Link, Location...  // 列出了哪些首部可以作为响应的一部分暴露给外部
Cache-Control: no-cache                                 // 缓存失效时间
Content-Length: 5                                       // 获取文件的总大小
Content-Security-Policy: default-src 'none'             // 配置内容安全策略涉
Content-Type: application/json; charset=utf-8           // 返回数据格式
Date: Wed, 21 Nov 2018 09:55:47 GMT
Referrer-Policy: origin-when-cross-origin, strict-origin-when-cross-origin
Server: GitHub.com
Status: 200 OK                                          // 状态码
Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
Vary: Accept-Encoding
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-GitHub-Media-Type: github.v3; format=json
X-GitHub-Request-Id: A3D4:2AE5:13372C:19E45C:5BF52BA3
X-XSS-Protection: 1; mode=block

HTTP方法相关?

GET请求资源、post传输资源、put更新资源、delete删除资源、head获取报文首部

get和post区别

  • get只能url 编码、post支持多种编码方式
  • get在传输参数有长度限制的,而post是没有长度限制的
  • get通过url传递,post放在request body中
  • get不安全,post是一种安全的传输协议方式
  • get会把参数保存到浏览器记录里,而post中的参数不会保存
  • get 会被浏览器缓存

http 常见状态码有哪些?

1.XX:指示信息-表示请求已经接受,继续处理

2.XX:成功
200:请求成功
206:客户端发送一个range头的get请求,服务器完成了他

3.XX:重定向
301:请求的页面转移到新的url;
302:临时转移到新的url
304:客户端缓存的文件并发出了一个条件性的请求,服务器告诉客户,原来缓冲的文档还可以继续使用

4.XX:客户端错误
400:语法错误
401:请求未授权
403:请求禁止访问
404:请求资源不存在

5.XX:服务端错误
500:服务器发生不可预期的错误
503:服务器请求未完成

什么是 HTTP持久链接?

http采用的是 "请求-应答" 模式
当使用keep-Alive 模式(又称持久链接、链接重用)时、http1.1版本才支持的
Connection: keep-alive

什么是管线化?

持久链接下:链接传递消息类似于请求1->响应1->请求2->响应2->请求3->响应3
管线化:请求1-》请求2-》请求3-》响应1-》响应2-》响应3
需要通过持久链接完成,所以仅HTTP1.1版本支持
只有get和head请求支持管线化,post请求是有所限制的

深入研究 HTTPS

Https涉及到的主体:

1、客户端。通常是浏览器(Chrome、IE、FireFox等),也可以自己编写的各种语言的客户端程序。
2、服务端。一般指支持Https的网站,比如github、支付宝。
3、CA(Certificate Authorities)机构。Https证书签发和管理机构,比如Symantec、Comodo、GoDaddy、GlobalSign。

图示这三个角色:
01-05-01

发明 Https 的动机:

认证正在访问的网站。 什么叫认证网站?比如你正在访问支付宝,怎样确定你正在访问的是阿里巴巴提供的支付宝而不是假冒伪劣的钓鱼网站呢?
保证所传输数据的私密性和完整性。 众所周知,Http是明文传输的,所以处在同一网络中的其它用户可以通过网络抓包来窃取和篡改数据包的内容,
甚至运营商或者wifi提供者,有可能会篡改http报文,添加广告等信息以达到盈利的目的。

Https的工作流程

01-05-02

可以看到工作流程,基本分为三个阶段

1、认证服务器。 浏览器内置一个受信任的CA机构列表,并保存了这些CA机构的证书。
第一阶段服务器会提供经CA机构认证颁发的服务器证书,如果认证该服务器证书的CA机构,存在于浏览器的受信任CA机构列表中,
并且服务器证书中的信息与当前正在访问的网站(域名等)一致,那么浏览器就认为服务端是可信的,
并从服务器证书中取得服务器公钥,用于后续流程。
否则,浏览器将提示用户,根据用户的选择,决定是否继续。
当然,我们可以管理这个受信任CA机构列表,添加我们想要信任的CA机构,或者移除我们不信任的CA机构。

2、协商会话密钥。 客户端在认证完服务器,获得服务器的公钥之后,利用该公钥与服务器进行加密通信,
协商出两个会话密钥,分别是用于加密客户端往服务端发送数据的客户端会话密钥,用于加密服务端往客户端发送数据的服务端会话密钥。
在已有服务器公钥,可以加密通讯的前提下,还要协商两个对称密钥的原因,是因为非对称加密相对复杂度更高,在数据传输过程中,使用对称加密,可以节省计算资源。
另外,会话密钥是随机生成,每次协商都会有不一样的结果,所以安全性也比较高。

**3、加密通讯。**此时客户端服务器双方都有了本次通讯的会话密钥,之后传输的所有Http数据,都通过会话密钥加密。
这样网路上的其它用户,将很难窃取和篡改客户端和服务端之间传输的数据,从而保证了数据的私密性和完整性。

总结

说是讨论Https,事实上Https就是Http跑在SSL或者TLS上,所以本文讨论的原理和流程其实是SSL和TLS的流程,对于其它使用SSL或者TLS的应用层协议,本文内容一样有效。
本文只讨论了客户端验证服务端,服务端也可以给客户端颁发证书并验证客户端,做双向验证,但应用没有那么广泛,原理类似。
由于采用了加密通讯,Https无疑要比Http更耗费服务器资源,这也是很多公司明明支持Https却默认提供Http的原因。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.