普通视图

发现新文章,点击刷新页面。
昨天 — 2026年1月18日首页

JS-彻底告别跨域烦恼:从同源策略到 CORS 深度实战

2026年1月18日 17:12

前言

在 Web 开发中,“跨域”是每个前端开发者绕不开的坎。当你看到控制台报出 Access-Control-Allow-Origin 错误时,其实是浏览器的同源策略在起作用。本文将带你深度解析跨域的本质,并掌握主流的解决方案。

一、 什么是跨域?

1. 同源策略 (Same-origin policy)

跨域问题的根源是浏览器为了安全而实施的同源策略。所谓“同源”,是指两个 URL 的以下三部分完全相同:

  • 协议:http 、 https
  • 域名 :域名就是我们每次访问网站输入的网址,每个域名都对应了一个IP地址,浏览器会通过域名解析来获取这个IP地址,例如www.test.com
  • 端口号 :80 、 8080

2. 域名解析小科普

域名是 IP 地址的“外壳”。

  • 顶级域名:.com, .cn
  • 一级域名:test.com
  • 二级域名www.test.com
  • 注意:一级域名和二级域名之间、二级域名和三级域名之间,统统属于跨域!比如在www.test.com网页使用 XMLHttpRequest 请求time.test.con的页面内容,由于它们不是同一个源,所以就涉及到了跨域(在 A 站点中去访问不同源的 B 站点的内容)。默认情况下,跨域请求是不被允许的,你可以看下面的示例代码:

二、 解决方案一:JSONP

1. 实现原理

利用 <script> 标签的 src 属性不受同源策略限制的特性。通过动态创建 script 标签,发送一个带有 callback 参数的 GET 请求。

2. 代码实现

前端逻辑:

btn.click(() => {
  var script = document.createElement("script");// 创建 scrip 标签
  script.src = `http://localhost:3000?callback=show`;// 添加 src 请求路径
  document.body.appendChild(script);
  script.onload = function(){
    document.body.removeChild(script)
  }
});

//这个函数就是回调函数,它会拼接到src属性中,并对数据进行操作
function show(result) {
  // ...
console.log("获取到的数据:", result);
}

服务端逻辑:

const http = require("http")
const url = require("url")
http.createServer(
  (req,res)=>{
    var callback = url.parse(req.url,true).query.callback;
    var severData = "xxxxxxxx";
    severData = JSON.stringify(severData)
    res.writeHead(200,{
      "Content-Type": "text/plain;charset=utf-8"
    });
    res.write(`${callback}(${severData})`);
    res.end();
  }
).listen(80)

局限性仅支持 GET 请求,不安全,且无法处理复杂的报错信息。


三、 解决方案二:CORS (现代的标准方案)

CORS(跨域资源共享)是目前的标准解法。它将请求分为简单请求非简单请求

1. 简单请求

条件:方法为 GET/POST

  • 流程:浏览器直接发起请求,并在 Header 中带上 Origin
  • 服务端:通过返回 Access-Control-Allow-Origin 来告知浏览器是否放行。

2. 非简单请求(预检请求)

条件:包含 PUT/DELETE 方法。

  • 流程:浏览器会先发送一个 OPTIONS 方法的“预检请求”。

  • 关键字段

    • Access-Control-Max-Age: 设置预检请求的缓存时间(秒),避免每次请求都多发一次 OPTIONS,优化性能。

3. Nginx 服务端配置示例

server {
    listen 80;
    location / {
        # 允许跨域的域名,建议生产环境指定具体域名而非 *
        add_header 'Access-Control-Allow-Origin' '$http_origin';
        add_header 'Access-Control-Allow-Credentials' 'true';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
        
        # 处理预检请求
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Max-Age' 1728000;
            return 204;
        }
    }
}

四、 面试模拟题

Q1:为什么要有同源策略?如果没有会怎样?

参考回答:

同源策略主要是为了防止 CSRF(跨站请求伪造) 攻击。如果没有同源策略,黑客的网页可以随意读取你银行网页的 Cookie 或 DOM 内容,从而冒充你发送请求或窃取敏感信息。

Q2:CORS 预检请求(OPTIONS)在什么情况下会触发?

参考回答:

当请求满足以下任意条件时会触发预检:

  1. 使用了 PUTDELETECONNECTOPTIONSTRACEPATCH 方法。
  2. 设置了非简单的 Header 字段(如 Authorization、自定义 Token)。
  3. Content-Type 的值不属于 application/x-www-form-urlencodedmultipart/form-datatext/plain

Q3:如何解决跨域时 Cookie 无法携带的问题?

参考回答:

  1. 前端XMLHttpRequestfetch 需设置 withCredentials: true
  2. 服务端:设置响应头 Access-Control-Allow-Credentials: true
  3. 注意:当开启凭证携带时,Access-Control-Allow-Origin 不能设置为 * ,必须指定具体的域名。

五、 总结

方案 原理 优点 缺点
JSONP <script> 标签不受限 兼容性极好(老浏览器) 只支持 GET,安全性差
CORS 服务端 Header 授权 正式标准,支持所有方法 需服务端配合,有预检开销

JS-深度解构JS事件循环(Event Loop)

2026年1月17日 22:58

前言

为什么 JavaScript 是单线程的却能处理异步 IO?为什么 setTimeout 并不总是准时?本文将从宏观的执行栈、任务队列,一直深入到浏览器底层的任务调度逻辑,带你彻底看透事件循环。

一、 为什么需要事件循环?

JavaScript 的核心是单线程的,这意味着它只有一个主线程来处理 DOM 解析、样式计算、脚本执行等。如果某个任务耗时过长,页面就会“卡死”。为了协调同步任务与异步任务(输入事件、网络请求、定时器),浏览器引入了事件循环系统来统一调度和处理这些任务。


二、 核心组件:执行栈与任务队列

1. 执行栈 (Execution Stack)

当多个方法被调用的时候,因为js是单线程的,所以每次只能执行一个方法,于是这些方法被排到了一个单独的地方,这个地方就是执行栈。执行栈里面执行的都是同步的操作。

2. 事件队列 (Task Queue)

  • 在js执行过程中如果遇到异步事件(如 Ajax、定时器),就会首先将这个异步事件交给对应的浏览器模块(如网络进程),继续执行执行栈里面的任务。
  • 当异步事件返回结果后,js不会立即执行这个回调,会将事件加入到事件队列中,只有当执行栈里面的全部执行完以后,主线程才会去查找事件队列中是否有任务。
  • 如果有,那么主线程会取出事件队列里面排在最前面的事件,将这个事件对应的回调加入到执行栈中,然后执行其中的同步代码。然后在继续观察执行栈里面是否有任务,依次反复...就形成了一个无限的循环。
  • 这就是这个过程被称为事件循环(Event loop)的原因。

循环逻辑:

  1. 检查执行栈是否为空。
  2. 若为空,从事件队列头部取出一个任务推入执行栈。
  3. 循环往复。

三、 异步任务的“等级”:宏任务与微任务

并非所有的异步任务优先级都一样。在同一次循环中,微任务永远在下一次宏任务之前执行!!!

类型 包含任务 执行时机
宏任务 (MacroTask) setTimeout, setInterval, ajax, dom事件 每次事件循环开始时处理一个
微任务 (MicroTask) Promise.then/catch, MutaionObserver, process.nextTick (Node.js) 当前执行栈清空后,立即清空整个微任务队列

注意: new Promise() 构造函数内部的代码是同步执行的,只有 .then().catch() 里的回调才是微任务。(后续会专门出一篇promise相关文章)


四、 底层揭秘:定时器是如何实现的?

很多开发者认为 setTimeout 是直接进入消息队列的,但浏览器底层其实维护了一个延迟执行队列 (Delayed Incoming Queue)

1. 任务数据结构

当调用 setTimeout 时,渲染进程内部会创建一个任务结构体:

struct DelayTask{
  int64 id;
  CallBackFunction cbf;
  int start_time;
  int delay_time;
};

2. 执行循环模拟

浏览器的主线程循环逻辑伪代码如下:

void MainThread() {
  for(;;) {
    // 1. 执行普通消息队列中的一个任务 (宏任务)
    Task task = task_queue.takeTask();
    ProcessTask(task);
    
    // 2. 执行微任务队列 (本阶段由 JS 引擎控制)
    // ProcessMicrotasks(); 

    // 3. 执行延迟队列中到期的任务 (定时器任务在此处理)
    ProcessDelayTask();

    if(!keep_running) break; 
  }
}

关键点: 浏览器会在处理完一个普通宏任务后,去检查延迟队列中是否有任务到期(ProcessDelayTask),并依次执行它们。


五、 面试模拟题

Q1:为什么 setTimeout(fn, 0) 并不一定是 0ms 后执行?

参考回答:

  1. 浏览器最小限制:HTML5 规范规定,如果定时器嵌套超过 5 层,最小延迟为 4ms。
  2. Event Loop 阻塞:由于定时器任务是在 ProcessDelayTask 中处理的,如果当前的宏任务(比如一个复杂的计算循环)执行时间过长,主线程就无法及时跳转到延迟队列的检查步骤,导致定时器推迟执行。

Q2:说出以下代码的打印顺序:

console.log('1');
setTimeout(() => console.log('2'), 0);
Promise.resolve().then(() => console.log('3'));
console.log('4');

参考回答:

1 -> 4 -> 3 -> 2。

  • 1, 4 是同步任务,直接输出。
  • 3 是微任务,在当前脚本(宏任务)执行完后立即执行。
  • 2 是下一次宏任务。

Q3:MutationObserver 属于什么任务?它有什么应用场景?

参考回答:

MutationObserver 属于微任务。它用于监听 DOM 树的变化。由于它是微任务,它会在 DOM 变化引起的多次修改全部完成后,在浏览器重新渲染之前异步执行,这比传统的 Mutation Events 性能更高,且不会阻塞主线程渲染。


六、 总结建议

  • 理解微任务的优先级:微任务是在当前宏任务结束后的“插队”行为,适合处理需要立即反馈的异步逻辑。

JS-new 操作符

2026年1月17日 22:26

前言

在 JavaScript 面向对象编程中,new 关键字是实例化对象的核心。面试官常常通过“手写 new”来考察你对原型链this 绑定以及构造函数返回值的理解。本文将带你从原理到实现,彻底搞懂 new 背后的魔法。

一、 new 到底干了什么?

当我们使用 new Person() 时,JS 引擎在背后默默执行了以下 4 个步骤

  1. 创建一个新对象:在内存中创建一个新的空对象(例如 obj = {})。

  2. 链接原型:将新对象的 __proto__ 属性指向构造函数的 prototype,从而实现原型继承(让实例能访问原型上的方法)。

  3. 绑定 this:将构造函数内部的 this 绑定到这个新对象上,并执行构造函数(为新对象添加属性)。

  4. 返回对象

    • 如果构造函数显式返回了一个对象(或函数),则返回该结果。
    • 如果构造函数没有返回对象(返回基本类型或无返回值),则返回步骤 1 创建的新对象

二、 手写 myNew 实现

根据上述原理,我们可以实现一个自己的 myNew 函数。

/**
 * 手写 new 操作符
 * @param {Function} Constructor 构造函数
 * @param  {...any} args 传递的参数
 */
function myNew(Constructor, ...args) {
  // 1. 创建一个新对象,并将其原型指向构造函数的 prototype
  const obj = Object.create(Constructor.prototype);

  // 2. 将构造函数的 this 绑定到新对象上,并执行构造函数
  const result = Constructor.apply(obj, args);

  // 3. 处理返回值逻辑 (这是面试中最容易忽视的细节!)
  // 如果构造函数返回的是对象(不为null)或函数,则返回该结果;否则返回新创建的 obj
  if ((typeof result === 'object' && result !== null) || typeof result === 'function') {
    return result;
  }
  
  // 4. 返回新对象
  return obj;
}

测试用例:

function Person(name, age) {
  this.name = name;
  this.age = age;
  // 情况 1: 没有返回值(默认返回 this)
}

function Student(name) {
  this.name = name;
  // 情况 2: 返回一个对象
  return { name: 'Special Student', grade: 100 };
}

function NumberObj() {
  this.a = 1;
  // 情况 3: 返回一个基本类型
  return 123;
}

// 测试 1:正常情况
const per = myNew(Person, 'Ouyang', 23);
console.log(per); // Person { name: 'Ouyang', age: 23 }
console.log(per instanceof Person); // true

// 测试 2:构造函数返回对象
const stu = myNew(Student, 'XiaoMing');
console.log(stu); // { name: 'Special Student', grade: 100 } (this 被忽略了)

// 测试 3:构造函数返回基本类型
const num = myNew(NumberObj);
console.log(num); // NumberObj { a: 1 } (返回值 123 被忽略)

三、 深度解析:返回值陷阱

这是面试中最常挖的坑。

  • 场景 A:构造函数内部没有 return,或者 return 一个基本数据类型(Number, String, Boolean, null, undefined)。

    • 结果new 操作符会忽略这个返回值,直接返回新创建的实例对象
  • 场景 B:构造函数内部 return 一个引用类型(Object, Array, Function)。

    • 结果new 操作符会直接返回这个引用类型,新创建的实例对象会被丢弃(且 this 上的属性赋值也会失效)。

四、 面试模拟题(挑战一下)

Q1:Object.create()new 有什么区别?

参考回答:

  • new:不仅创建新对象并继承原型,还会执行构造函数,进行属性初始化。
  • Object.create():只负责创建一个新对象并继承原型,不会执行构造函数

Q2:为什么代码中建议使用 Object.create 而不是 obj.__proto__

参考回答: __proto__ 是非标准属性(虽然浏览器支持),直接修改它会破坏 JS 引擎的优化,严重影响性能。Object.create() 是 ES5 标准方法,更规范且性能更好。

Q3:如果构造函数返回 nullnew 出来的结果是什么?

参考回答: 结果是新创建的实例对象。 因为 typeof null === 'object',但 null 是个特殊值。在 new 的规范中,如果返回的是对象类型但值为 null,仍然会忽略它,返回实例对象。这就是为什么在手写代码中我们要判断 result !== null


结语

手写 new 是前端基础能力的试金石。理解了这 4 个步骤,你不仅能轻松应对面试,还能更深刻地理解 JavaScript 的继承机制。

如果你觉得这篇笔记对你有帮助,欢迎点赞收藏! 🚀

JS -彻底搞懂 call、apply、bind 的区别与应用

2026年1月17日 22:13

前言

在 JavaScript 中,this 的指向是动态的,这虽然灵活,但也常让我们头疼。而 callapplybind 就是我们手中的“魔法棒”,专门用来手动控制 this 的指向。它们有什么区别?分别在什么场景下使用?本文带你一探究竟。

一、 三大方法详解

这三个方法都挂载在 Function.prototype 上,这意味着所有的函数都可以调用它们。

1. call()

  • 作用:修改函数的 this 指向,并立即执行该函数。

  • 参数

    1. thisArgthis 需要绑定的对象。
    2. arg1, arg2, ...参数列表,直接按顺序传入。
  • 默认行为:如果不传 thisArg 或传 null/undefined,在非严格模式下指向 window

fn.call(obj, agr1,agr2,arg3,arg4,.....)

2. apply()

  • 作用:修改函数的 this 指向,并立即执行该函数。

  • 参数

    1. thisArgthis 需要绑定的对象。
    2. argsArray数组(或类数组) ,数组内的元素会被展开传入函数。
fn.apply(obj, [agr1,agr2,arg3,arg4,.....])

3. bind()

  • 作用:修改函数的 this 指向,但不会立即执行
  • 返回值:返回一个新的函数(称为绑定函数)。
  • 硬绑定bind 返回的新函数,其 this 指向一旦被绑定,后续再使用 callapply 都无法再次修改。
  • 参数:与 call 相同,接受参数列表。支持柯里化(预设部分参数)。
bind(thisArg, arg1, arg2, arg3, ...)

二、 核心区别对比(一张表看懂)

方法 执行时机 参数格式 返回值 核心场景
call 立即执行 参数列表 (arg1, arg2) 函数执行结果 对象继承、借用方法
apply 立即执行 数组 ([arg1, arg2]) 函数执行结果 数学计算、数组合并
bind 稍后执行 参数列表 (arg1, arg2) 新函数 事件绑定、回调函数

三、 代码实战与纠错

让我们通过一个经典的例子来看它们的具体表现。

const obj = {
  name: 'Original',
  fn: function(a, b) {
    console.log(this.name, a, b);
  }
}

const db = { name: 'DataBase' };

// 1. 原始调用
obj.fn(1, 2); 
// 输出: "Original" 1 2

// 2. call 调用:传参列表
obj.fn.call(db, 3, 4); 
// 输出: "DataBase" 3 4

// 3. apply 调用:传参数组
obj.fn.apply(db, [5, 6]); 
// 输出: "DataBase" 5 6

// 4. bind 调用:返回新函数,手动执行
const boundFn = obj.fn.bind(db, 7, 8);
boundFn(); 
// 输出: "DataBase" 7 8

// 5. bind 的连续修改无效性(面试坑点)
const doubleBind = obj.fn.bind(db).bind({ name: 'Error' });
doubleBind();
// 输出: "DataBase" undefined undefined (第二次 bind 无效)

四、 常见应用场景(面试加分项)

仅仅知道语法是不够的,面试官更看重你知道怎么用。

1. 数组求最大值 (apply)

利用 apply 接受数组参数的特性,结合 Math.max

const nums = [5, 10, 20, 1];
const max = Math.max.apply(null, nums); // 20
// ES6 写法: Math.max(...nums)

2. 类数组转数组 (call)

利用 call 借用数组的 slice 方法。

function func() {
  const args = Array.prototype.slice.call(arguments);
  console.log(args); // 变成了真数组
}

3. React/Vue 中的事件绑定 (bind)

防止回调函数在执行时 this 丢失(指向 undefinedwindow)。

this.handleClick = this.handleClick.bind(this);

五、 面试模拟题

Q1:callapply 的唯一区别是什么?

参考回答:

它们的唯一区别在于传参方式。call 需要把参数按顺序一个个传进去(参数列表),而 apply 需要把参数放在一个数组(或类数组)里传进去。助记口诀:"a" for array (apply), "c" for comma (call)。

Q2:为什么 bind 返回的函数,再次使用 call 无法修改 this

参考回答:

这涉及 bind 的内部实现。bind 返回的函数内部已经通过闭包锁定了 this(通常称为硬绑定)。也就是类似 return function() { return originalFn.apply(that, arguments) } 的结构。无论外部怎么 call,内部的 apply 永远使用的是第一次绑定的 that。

Q3:手写一个简单的 bind

      Function.prototype.myBind = function (context, ...args) {
        // 1. 保存当前的函数(this 指向原函数)
        const fn = this;
        // 2. 返回一个新的函数
        return function (...innerArgs) {
          // 3. 将预设参数和新参数合并,并用 apply 执行原函数
          return fn.apply(context, args.concat(innerArgs));
        };
      };
      const obj = {
        name: "Original",
        fn: function (a, b) {
          console.log(this.name, a, b);
        },
      };
      const boundFn = obj.fn.myBind({ name: 'DataBase' }, 7, 8);
      boundFn();
❌
❌