原型链与继承

JavaScript 中没有类的概念的,主要通过原型链来实现继承。通常情况下,继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联(原型对象指针),这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。 原型 当我们 new 了一个新的对象实例,明明什么都没有做,就直接可以访问 toString 、valueOf 等原生方法。那么这些方法是从哪里来的呢?答案就是原型。 在控制台打印一个空对象时,我们可以看到,有很多方法,已经“初始化”挂载在内置的 __proto__ 对象上了。这个内置的 __proto__ 是一个指向原型对象的指针,它会在创建一个新的引用类型对象时(显示或者隐式)自动创建,并挂载到新实例上。当我们尝试访问实例对象上的某一属性 / 方法时,如果实例对象上有该属性 / 方法时,就返回实例属性 / 方法,如果没有,就去 __proto__ 指向的原型对象上查找对应的属性 / 方法。这就是为什么我们尝试访问空对象的 toString 和 valueOf 等方法依旧能访问到的原因,JavaScript 正式以这种方式为基础来实现继承的。 构造函数 如果说实例的 __proto__ 只是一个指向原型对象的指针,那就说明在此之前原型对象就已经创建了,那么原型对象是什么时候被创建的呢?这就要引入构造函数的概念。 其实构造函数也就只是一个普通的函数而已,如果这个函数可以使用 new 关键字来创建它的实例对象,那么我们就把这种函数称为 构造函数。 // 普通函数 function person () {} // 构造函数,函数首字母通常大写 function Person () {} const person = new Person(); 原型对象正是在构造函数被声明时一同创建的。构造函数被申明时,原型对象也一同完成创建,然后挂载到构造函数的 prototype 属性上: 原型对象被创建时,会自动生成一个 constructor 属性,指向创建它的构造函数。这样它俩的关系就被紧密地关联起来了。 细心的话,你可能会发现,原型对象也有自己的 __proto__ ,这也不奇怪,毕竟万物皆对象嘛。原型对象的 proto 指向的是 Object.prototype。那么 Object.prototype.__proto__ 存不存在呢?其实是不存在的,打印的话会发现是 null 。这也证明了 Object 是 JavaScript 中数据类型的起源。 ...

October 1, 2024

前端组件化开发

背景 不知道你有没有遇到过以下场景: 页面逻辑越来越多,代码越来越庞大,写到后面难以 hold 住所有逻辑,很容易牵一发而动全身. 你负责的页面好好的突然出现问题,查到最后是别人代码影响. 同样的逻辑在多个地方重复书写,每次一改要改一批文件 随着前端项目复杂度的急剧增加, 上面列出的几种场景就是传统开发中会出现的问题. 也是前端组件化出现的原因. 项目复杂度增加, 一个页面一个文件需要处理的内容过多. 重复性劳动多, 效率低 质量差, 不可控 组件化初探 正是由于出现了这样的问题, 为了在越来越复杂的前端项目中提高开发效率和保证开发质量, 各路大神们开始通过各种方式来尝试解决问题. 曾经非常火的 jQuery 就基于自己建立了 jQuery 插件机制. 你可以将一些常用逻辑进行封装变成 jQuery 插件, 还可以将插件开源进行共享. 比如纯手写会吐血的日期时间选择器,轮播, 多级菜单等等. 你可以在这里浏览更多jQuery插件. jQuery 插件的用法通常是: $(".select").pluginName(config) 除了 jQuery 插件模式, 还有一种常见模式是对象模式. 这种模式现在仍然有很多优秀的库在被我们直接或者间接使用. 比如: swiper 对象模式的写法: <!-- Slider main container --> <div class="swiper-container"> <!-- Additional required wrapper --> <div class="swiper-wrapper"> <!-- Slides --> <div class="swiper-slide">Slide 1</div> <div class="swiper-slide">Slide 2</div> <div class="swiper-slide">Slide 3</div> ... </div> </div> <script src="path/to/swiper.min.js"></script> <script> var mySwiper = new Swiper ('.swiper-container', { direction: 'vertical', loop: true, }) </script> 对象模式通过配置创建对象, 通常创建对象的参数中有一项是元素/元素选择器, 通过js代码将逻辑与这个元素紧密绑定. ...

August 7, 2025

CDN原理

CDN CDN 即内容分发网络(Content Delivery Network)的简称,是建立在承载网基础上的虚拟分布式网络,能够将源站内容(包括各类动静态资源)智能缓存到全球各节点服务器上。这样不仅方便了用户就近获取内容,提高了资源的访问速度,也分担了源站压力。 CNAME、A 记录、NS 记录 DNS ( Domain Name System,域名系统)提供了将域名转换为 IP 地址的服务。为了完成这个转化工作,DNS 的数据库中需要维护相关的数据,这些数据被叫做 RR(Resource Record,资源记录)。资源记录有很多种类型,比如 A、NS、SOA、CNAME 和 PTR 记录。 大家接触最多的就是 A(Address)记录。A 记录是一条从域名到 IP 地址的映射记录。而 CNAME(Canonical Name)记录是一条从域名到域名的映射记录。它在 CDN 技术中有举足轻重的作用,很好地实现了业务域名与 CDN 系统域名的解耦。简单理解,如果一个域名配置了 A 记录,DNS 就会把它解析成 A 记录指定的 IP 地址;如果一个域名配置了 CNAME 记录,DNS 就会把它解析成 CNAME 记录指定的另外一个域名;A 记录和 CNAME 记录是互斥的,不能同时存在。 NS (Name Server)记录是和 DNS 服务器相关的一条记录,它指定该域名应该由哪一台 DNS 服务器进行解析。一般把通过 NS 记录指定的 DNS 服务器叫做该域名的权威 DNS 服务器。 加速域名 加速域名指需要使用 CDN 加速的域名。加速域名也是一个域名。加速域名一般配置 CNAME 记录,指向 CDN 网络节点。普通域名一般配置 A 记录,指向提供服务的业务服务器。 ...

July 28, 2025

Fragment 和 Portals

一、Portals 某些情况下,我们希望渲染的内容独立于父组件,甚至独立于当前挂在的DOM元素(默认都是挂载到id为root的DOM元素上的) Portal提供了一种将子节点渲染到存在于父组件以外的DOM节点的优秀的方案: ReactDOM.createPortal(child, container) 第一个参数:是任何可渲染的React子元素,例如一个元素、字符串或fragment 第二个参数:是一个DOM元素 通常来讲,当你从组件的 render 方法返回一个元素时,该元素将被挂载到 DOM 节点中离其最近的父节点: render() { // React 挂载了一个新的 div,并且把子元素渲染其中 return ( <div> {this.props.children} </div> ); } 然而,有时候将子元素插入到 DOM 节点中的不同位置也是有好处的: render() { // React 并没有创建一个新的 div。它只是把子元素渲染到 `domNode` 中。 // `domNode` 是一个可以在任何位置的有效 DOM 节点。 return ReactDOM.createPortal( this.props.children, domNode ); } 一个 portal 的典型用例是当父组件有 overflow: hidden 或 z-index 样式时,但你需要子组件能够在视觉上“跳出”其容器。例如,对话框、悬浮卡以及提示框 案例 比如将h2挂在到id为zs的节点下 index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="theme-color" content="#000000" /> <meta name="description" content="Web site created using create-react-app" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <title>React App</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <div id="zs"></div> </body> </html> App.jsx ...

December 17, 2024

浏览器的安全性

浏览器安全性是前端开发中的重要考察点之一,主要指浏览器在访问网站过程中如何防止攻击者利用漏洞或机制实施攻击、窃取数据、破坏用户体验等。 一、常见的浏览器安全威胁 1. XSS(跨站脚本攻击) 原理:攻击者注入恶意脚本到网页中,在用户浏览页面时执行。 危害:窃取 cookie、伪造操作、传播蠕虫。 防御: 对输出进行HTML转义; 使用 Content Security Policy(CSP); 严格控制用户输入(白名单); 使用框架自动防御(如 React 的 JSX 自动转义)。 2. CSRF(跨站请求伪造) 原理:用户登录目标网站后,被诱导访问恶意链接,触发网站上的有状态请求。 危害:修改密码、转账等敏感操作被伪造。 防御: 使用 CSRF Token; Referer 验证; SameSite Cookie 属性限制第三方请求。 3. 点击劫持(Clickjacking) 原理:攻击者在页面上嵌入透明 iframe,引诱用户点击。 防御: 禁止网页被嵌入 iframe:X-Frame-Options: DENY / SAMEORIGIN; 使用 CSP 中的 frame-ancestors 指定允许嵌入的来源。 4. 恶意文件上传 原理:上传可执行脚本,触发服务端或客户端执行。 防御: 严格限制文件类型与大小; 不在上传目录下执行脚本; 设置 CDN 或存储桶只读访问权限。 5. 恶意第三方脚本(供应链攻击) 原理:攻击者污染 CDN 或依赖源,注入恶意代码。 防御: 使用子资源完整性校验(Subresource Integrity, SRI); 只信任可靠的依赖源; 上线前锁定依赖版本。 二、浏览器原生安全机制 1. 同源策略(Same-Origin Policy) 限制不同源之间访问 Cookie、DOM、LocalStorage 等。 同源指:协议、域名、端口号都相同。 2. CORS(跨域资源共享) 浏览器通过预检请求和响应头判断是否允许跨域访问。 3. Content Security Policy(CSP) 通过设置 HTTP Header 控制资源加载策略,防止 XSS 和数据泄露。 示例: Content-Security-Policy: default-src 'self'; script-src 'self' https://trust.cdn.com; 4. HTTP-only & Secure Cookie HttpOnly: 防止 JavaScript 读取 Cookie; Secure: 仅在 HTTPS 下传输 Cookie; SameSite: 限制第三方请求携带 Cookie。 5. Sandbox(iframe 安全沙箱) <iframe sandbox> 属性限制 iframe 的行为; 可防止脚本执行、表单提交等危险操作。 三、前端开发中的安全实践 安全措施 说明 输入校验 客户端和服务端都要做,优先使用白名单策略 输出编码 HTML、JavaScript、URL 编码避免 XSS 注入 HTTPS 加密传输防止中间人攻击(MITM) 使用现代框架 React/Vue/Angular 等框架天然防御 XSS 限制权限 用户行为应严格授权与验证 CSP 策略 强制资源加载来源、禁止内联脚本 四、总结 浏览器安全是前端必须掌握的重要基础知识,核心目标是 防止前端受到攻击者控制或操纵。它涉及浏览器机制、HTTP协议、安全头部、数据验证等多个维度,需要前端开发者在日常开发中养成良好安全意识与编码习惯。 ...

December 17, 2024

跨域通信

什么是跨域 在了解跨域之前,我们必须要了解一下同源策略。 跨域问题其实就是浏览器的同源策略造成的。 同源策略 同源策略限制了从同一个源加载的文档或脚本如何与另一个源的资源进行交互。这是浏览器的一个用于隔离潜在恶意文件的重要的安全机制。同源指的是:协议、端口号、域名必须一致。 跨源资源共享([CORS],或通俗地译为跨域资源共享)是一种基于 [HTTP] 头的机制,该机制通过允许服务器标示除了它自己以外的其他[源] (域、协议或端口),使得浏览器允许这些源访问加载自己的资源。跨源资源共享还通过一种机制来检查服务器是否会允许要发送的真实请求,该机制通过浏览器发起一个到服务器托管的跨源资源的“预检”请求。在预检中,浏览器发送的头中标示有 HTTP 方法和真实请求中会用到的头。 为什么会产生跨域 下表给出了与 URL wmq.com/dir/page.ht… 的源进行对比的示例: URL 是否跨域 原因 wmq.com/dir/page.ht… 同源 完全相同 wmq.com/dir/inner/a… 同源 只有路径不同 wmq.com/secure.html 跨域 协议不同 wmq.com:81/dir/etc.htm… 跨域 端口不同 ( http:// 默认端口是80) news.company.com/dir/other.h… 跨域 主机不同 同源策略:protocol(协议)、domain(域名)、port(端口)三者必须一致。 同源政策主要限制了三个方面: 当前域下的 js 脚本不能够访问其他域下的 cookie、localStorage 和 indexDB。 当前域下的 js 脚本不能够操作访问操作其他域下的 DOM。 当前域下 ajax 无法发送跨域请求。 源政策的目的主要是为了保证用户的信息安全,它只是对 js 脚本的一种限制,并不是对浏览器的限制,对于一般的 img、或者script 脚本请求都不会有跨域的限制,这是因为这些操作都不会通过响应结果来进行可能出现安全问题的操作。 有哪些解决跨域的办法 CORS CORS: 通过设置服务器端的响应头来允许跨域请求。这需要在服务器端进行配置,以允许特定的来源访问资源。例如,在Node.js的Express框架中,可以使用以下代码来设置CORS: const express = require('express'); const app = express(); app.use((req, res, next) => { res.header('Access-Control-Allow-Origin', '*'); // 允许所有来源访问 res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept'); // 允许的请求头 res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); // 允许的请求方法 next(); }); // 其他路由和中间件 app.listen(3000, () => { console.log('Server is running on port 3000'); }); 减少OPTIONS请求次数 在跨域请求中,浏览器会自动处理一些非简单请求的预检流程,这包括"预检请求"(也被称为CORS预检请求)和"OPTIONS请求"。预检请求是发起一个HTTP OPTIONS请求到服务器,以确认目标资源是否支持跨域。这种机制是为了兼容同源策略而产生的,但有时这种额外的请求会导致性能问题。 ...

October 14, 2024

Grid布局

Grid 布局是什么? Grid 布局即网格布局,是一种新的 CSS 布局模型,比较擅长将一个页面划分为几个主要区域,以及定义这些区域的大小、位置、层次等关系。号称是最强大的的 CSS 布局方案,是目前唯一一种 CSS 二维布局。利用 Grid 布局,我们可以轻松实现类似下图布局,演示地址 Grid 布局和 flex 布局 讲到布局,我们就会想到 flex 布局,甚至有人认为竟然有 flex 布局了,似乎没有必要去了解 Grid 布局。但 flex 布局和 Grid 布局有实质的区别,那就是 flex 布局是一维布局,Grid 布局是二维布局。flex 布局一次只能处理一个维度上的元素布局,一行或者一列。Grid 布局是将容器划分成了“行”和“列”,产生了一个个的网格,我们可以将网格元素放在与这些行和列相关的位置上,从而达到我们布局的目的。 Grid 布局远比 flex 布局强大! flex布局示例: Grid 布局示例: Grid 的一些基础概念 我们使用 Grid 实现一个小例子,演示 Grid 的一些基础概念,演示地址 <div class="wrapper"> <div class="one item">One</div> <div class="two item">Two</div> <div class="three item">Three</div> <div class="four item">Four</div> <div class="five item">Five</div> <div class="six item">Six</div> </div> .wrapper { margin: 60px; /* 声明一个容器 */ display: grid; /* 声明列的宽度 */ grid-template-columns: repeat(3, 200px); /* 声明行间距和列间距 */ grid-gap: 20px; /* 声明行的高度 */ grid-template-rows: 100px 200px; } .one { background: #19CAAD; } .two { background: #8CC7B5; } .three { background: #D1BA74; } .four { background: #BEE7E9; } .five { background: #E6CEAC; } .six { background: #ECAD9E; } .item { text-align: center; font-size: 200%; color: #fff; } ...

October 8, 2024

vue事件处理

一、DOM 事件流 想必看过《红宝书》的前端 er 对标题都不会特别陌生,我们来看看定义:当一个 HTML 元素产生一个事件时,该事件会在树形结构的 DOM 上面沿着元素节点路径进行传播,事件所经过的路径结点都会收到该事件,这个传播过程可称为 DOM 事件流。 二、DOM 事件流模型 DOM 事件流分为 捕获型事件流 和 冒泡型事件流。两种事件流分别对应三阶段 DOM 事件流模型中的捕获阶段和冒泡阶段: 捕获阶段:事件从最外面的祖先节点依次传递到最里面的后代节点 目标阶段:真正的目标节点正在处理事件的阶段 冒泡阶段:事件又从最里面的后代节点逐层传出到最外面的祖先节点 三、Vue 之事件处理 Vue 中提供了事件绑定的语法糖,我们可以很简单地在标签中直接使用 @click="handleClick($event)" 就可以绑定点击事件。 而且在 Vue 里面,Vue 的事件触发默认为冒泡过程监听,意思就是上面 @click="handleClick($event)" 中点击事件的执行是冒泡过程触发的。 四、Vue 之事件修饰符 4.1 .capture 捕获监听器 <div @click="log(1)" @click.capture="log(1)" style="background-color: #00f"> <div @click="log(2)" @click.capture="log(2)" style="background-color: #66f"> <div @click="log(3)" @click.capture="log(3)" style="background-color: #ccf"> <div @click="log(4)" @click.capture="log(4)" style="background-color: #fff"> 点击这里 </div> </div> </div> </div> 控制台打印结果为:1 2 3 4 4 3 2 1 ...

October 5, 2024

Map与Set

每当去学习ES6的时候,我都会不由自主的打开阮一峰老师的《ES6入门》去学习和查找用法,相信大多数同学也都和我一样看过阮老师写的文章。 当然大家也都知道ES6里常用的API就那么些,不多不少,用在项目中也是刚刚好。 不过在每次读到Set和Map数据结构那一章的时候,总是有点不知所措,因为我不明白实现这样的数据结构,目的是什么,意义又是什么呢 Set和Map主要的应用场景在于数组去重和数据存储,幸运的是在读了关于数据结构和算法之类的书籍后,恍然大悟的发现 原来Set是一种叫做集合的数据结构,Map是一种叫做字典的数据结构 那么下面就来了解下这两种数据结构,最后来亲手实现的一个ES6中的Set和Map吧 集合 集合是由一组无序且唯一(即不能重复)的项组成的,可以想象成集合是一个既没有重复元素,也没有顺序概念的数组 ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值 Set 本身是一个构造函数,用来生成 Set 数据结构 这里说的Set其实就是我们所要讲到的集合,先来看下基础用法 const s = new Set(); [2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x)); for (let i of s) { console.log(i); // 2 3 5 4 } // 去除数组的重复成员 let array = [1,2,1,4,5,3]; [...new Set(array)] // [1, 2, 4, 5, 3] 具体用法如果还有不清楚的,这里我会在后面一一细说。现在还是来看一下以ES6中Set类(数据结构)为基础实现的集合吧 Set实例的属性和方法 Set的属性: size:返回集合所包含元素的数量 Set的方法: 操作方法 add(value):向集合添加一个新的项 delete(value):从集合中移除一个值 has(value):如果值在集合中存在,返回true,否则false clear(): 移除集合里所有的项 遍历方法 keys():返回一个包含集合中所有键的数组 values():返回一个包含集合中所有值的数组 entries:返回一个包含集合中所有键值对的数组(感觉没什么用就不实现了) forEach():用于对集合成员执行某种操作,没有返回值 创建一个集合 function Set(arr = []) { // 可以传入数组 let items = {}; this.size = 0; // 记录集合中成员的数量 } module.exports = Set; 这里用{}对象来表示集合,也是因为对象不允许一个键指向两个不同的属性,保证了集合里的元素都是唯一的 ...

October 2, 2024

高阶函数

高阶函数是指那些可以接受函数作为参数或者返回函数作为结果的函数。这种特性使得JavaScript的函数变得异常灵活和强大,为开发者提供了丰富的工具和手段来编写更加简洁、可重用和模块化的代码。 本文将详细介绍JavaScript高阶函数的概念、特性、常见用法以及在实际开发中的应用。 一、高阶函数的概念 高阶函数是函数式编程中的一个核心概念。在JavaScript中,函数是一等公民,这意味着函数可以像其他数据类型(如数字、字符串、对象等)一样被传递和操作。高阶函数正是利用了这一特性,使得函数可以作为参数传递给其他函数,或者作为其他函数的返回值。 二、高阶函数的特性 函数作为参数:高阶函数可以接受一个或多个函数作为参数。这使得我们可以将特定的行为(即函数)作为参数传递给高阶函数,从而实现更加灵活和可重用的代码。 函数作为返回值:高阶函数可以返回一个函数作为结果。这种特性允许我们创建可以动态生成新函数的函数,这些新函数可以根据需要执行不同的行为。 闭包:闭包是高阶函数的一个重要特性。闭包允许函数访问其外部作用域中的变量,即使该函数在其外部作用域之外执行。这使得高阶函数可以捕获和存储状态,从而创建更加复杂和强大的函数行为。 三、高阶函数的常见用法 数组方法:JavaScript中的数组方法(如map、filter、reduce等)都是高阶函数的典型例子。这些方法接受一个函数作为参数,并对数组中的每个元素执行该函数,从而返回一个新的数组或单一结果。 map方法:用于创建一个新数组,其结果是该数组中的每个元素调用一个提供的函数后返回的结果。 filter方法:创建一个新数组,其包含通过所提供函数实现的测试的所有元素。 reduce方法:对数组中的每个元素执行一个由您提供的reducer函数(升序执行),将其结果汇总为单个返回值。 函数组合:函数组合是指将多个函数组合在一起形成一个新的函数。通过高阶函数,我们可以轻松地将多个函数组合在一起,从而创建一个更加复杂和强大的函数。这种技术在实际开发中非常有用,特别是当我们需要处理一系列转换或操作时。 柯里化:柯里化是一种将接收多个参数的函数转换为一系列接收单一参数的函数的技术。虽然柯里化本身不是高阶函数,但高阶函数在实现柯里化时扮演着重要角色。通过高阶函数,我们可以创建一个接受原始函数作为参数的柯里化函数,并返回一个新的函数来处理单一参数。 创建高阶函数:我们可以编写自己的高阶函数来实现特定的行为。例如,我们可以编写一个高阶函数来接受一个函数和一个数组作为参数,并返回该数组中应用该函数后的结果。 四、高阶函数在实际开发中的应用 1. 数据转换 高阶函数非常适合用于数据的转换和映射。例如,我们可以使用Array.prototype.map方法将一个数组中的每个元素转换为另一种形式。 // 定义一个高阶函数,它接受一个转换函数和一个数组作为参数 const transformArray = (transformFn, array) => array.map(transformFn); // 定义一个简单的转换函数,将数字转换为它的平方 const square = x => x * x; // 使用高阶函数和转换函数来处理数组 const numbers = [1, 2, 3, 4, 5]; const squaredNumbers = transformArray(square, numbers); console.log(squaredNumbers); // 输出: [1, 4, 9, 16, 25] 在这个例子中,transformArray是一个高阶函数,它接受一个转换函数transformFn和一个数组array作为参数。然后,它使用Array.prototype.map方法来遍历数组,并对每个元素应用转换函数。 2. 事件处理 在Web开发中,事件处理是一个常见的任务。通过高阶函数,我们可以创建更加灵活和可重用的事件处理器。 // 定义一个高阶函数,它接受一个事件处理函数和一个事件类型作为参数 const createEventHandler = (handlerFn, eventType) => event => { if (event.type === eventType) { handlerFn(event); } }; // 定义一个简单的事件处理函数,用于打印事件信息 const logEvent = event => console.log(`Event type: ${event.type}, Target: ${event.target}`); // 创建一个点击事件处理器 const handleClick = createEventHandler(logEvent, 'click'); // 假设我们有一个DOM元素,我们可以为它添加事件监听器 // const button = document.querySelector('button'); // button.addEventListener('click', handleClick); // 为了演示,我们直接调用处理器并传递一个模拟的事件对象 const mockEvent = { type: 'click', target: document.body }; handleClick(mockEvent); // 输出: Event type: click, Target: [object HTMLBodyElement] 在这个例子中,createEventHandler是一个高阶函数,它接受一个事件处理函数handlerFn和一个事件类型eventType作为参数,并返回一个新的函数。这个新的函数会检查传入的事件对象的类型是否与指定的事件类型匹配,如果匹配,则调用事件处理函数。 ...

October 1, 2024