JavaScript 字符串模板与数组操作虽优雅,也要谨防XSS

127 阅读3分钟

引言:从“拼接地狱”到优雅写法的进化之路

你有没有经历过这样的时刻?当你面对一段又臭又长的字符串拼接代码时,内心OS是:“这真的是我写的吗?”或者在处理数组数据的时候,用for循环一层套一层,最后自己都搞不清哪个变量对应哪个值了。

别担心,JavaScript 的 ES6(ECMAScript 2015)标准带来了两个非常实用的功能:字符串模板(Template Strings)数组的 map() 方法。它们不仅能让我们的代码更加简洁易读,还能显著提升开发效率和可维护性。

今天我们就来聊聊这两个神器,看看它们是如何帮助我们告别“拼接地狱”,轻松实现 DOM 操作与数据转换的

一、字符串模板 —— 让字符串也能有灵魂

1.1 曾经的痛苦:字符串拼接的噩梦

在 ES6 出现之前,我们通常使用加号 + 来拼接字符串:

var name = '旺财';
var age = 2;
var sentence = "My dog is " + name + ". He is " + (age * 7) + " years old.";

这种写法虽然功能没问题,但一旦遇到复杂的 HTML 或者大量变量嵌入,就会变得异常丑陋,甚至容易出错。而且遇到大量的数据填充,就会变得异常麻烦,写不完的+号和""

比如你要输出一个结构比较复杂的 div:

var html = '<div class="user-card">' +
    '<h3>' + user.name + '</h3>' +
    '<p>来自:' + user.city + '</p>' +
    '<p>年龄:' + user.age + '</p>' +
    '</div>';

看得我真的头大,”香菇“

1.2 新时代的曙光:字符串模板登场!

ES6 给我们带来了一个全新的语法——字符串模板(Template Strings) ,它使用反引号(`)包裹整个字符串,并通过 ${} 插入变量或表达式。

举个栗子🌰:

const name = '旺财';
const age = 2;

const sentence = `My dog ${name} is ${age * 7} years old.`;
console.log(sentence); // 输出:My dog 旺财 is 14 years old.

对比上述代码是不是更简洁明朗多了

而且字符串模板还支持多行文本,再也不用手动添加 \n 或者反复拼接了:

const poem = `春眠不觉晓,
处处闻啼鸟。
夜来风雨声,
花落知多少。`;

这段诗如果用传统方式写,估计得拼上天 😅 想想都觉得头大

1.3 实战应用:DOM 中的字符串模板

在实际项目中,字符串模板最常用于动态生成 HTML 内容。比如我们要展示一个朋友列表:

const friends = [
    { name: '小娄', hometown: '抚州' },
    { name: '小王', hometown: '上饶' },
    { name: '小刘', hometown: '赣州' }
];

我们可以结合模板字符串和 DOM 操作,快速构建页面内容:

<ul id="friends"></ul>
<script>
    const ul = document.querySelector('#friends');
    let html = '';
    for (let friend of friends) {
        html += `
            <li>
                ${friend.name} - 
                <i>${friend.hometown}</i>
            </li>
        `;
    }
    ul.innerHTML = html;
</script>

这样写是不是比以前清晰多了?而且格式也保留得很好,调试起来更方便,简单来说就是优雅哈哈哈哈

不过,如果你是一个追求极致优雅的程序员,那么接下来的内容会让你大呼过瘾,我们来告别传统的for循环,让代码更简洁

二、数组的 map() 方法 —— 数据变换的艺术

2.1 数组操作的痛点

假设你现在有一组 JSON 格式的数据,你想把它们转换成 HTML 元素插入到页面中。你会怎么做?

可能的做法是:

  • 用 for 循环遍历数组;
  • 每次取出一个元素;
  • 拼接成 HTML 字符串;
  • 最后赋值给 innerHTML。

没错,这确实能完成任务,但代码不够优雅,也不够现代

就如同下面的代码:

<ul id="friends"></ul>
    <script>
        // JSON 数组
        // 数据
        const friends = [
            { name: '小娄', hometown: '抚州' },
            { name: '小王', hometown: '上饶' },
            { name: '小刘', hometown: '赣州' }
        ]
        // DOM 编程
        const ul = document.querySelector('#friends')
        // 如果不需要index(下标),for of 更语义化
        //     for (let friend of friends) {

                 ul.innerHTML += `
         <li>
             ${friend.name} - 
             <i>${friend.hometown}</i>
         </li>
         `
             }

看着就比之前字符串+拼接更好,但是有没有更优雅的方法呢?有的,有的,兄弟有的,比这这更优雅的方法还有9种(牢九门)哈哈哈哈,其实是还有遍历数组更优雅的方法的,下面我就来给大家介绍

2.2 map() 初体验:让数组自己“变魔术”

map() 是 ES6 中新增的一个数组方法,它的作用是对数组中的每一个元素进行一次“映射”操作,并返回一个新的数组。

举个简单的例子:

const numbers = [1, 2, 3, 4];
const doubled = numbers.map(n => n * 2);
console.log(doubled); // [2, 4, 6, 8]

是不是很神奇?我们没有修改原数组,而是得到了一个新的数组

2.3 map() 在 DOM 编程中的实战

回到前面的朋友列表问题,我们可以用 map() 更优雅地处理:

const friends = [
    { name: '小娄', hometown: '抚州' },
    { name: '小王', hometown: '上饶' },
    { name: '小刘', hometown: '赣州' }
];

const ul = document.querySelector('#friends');

// 使用 map 把每个对象转成 li 元素字符串
const listItems = friends.map(friend => `
    <li>
        ${friend.name} - 
        <i>${friend.hometown}</i>
    </li>
`);

// 因为 map 返回的是数组,所以需要用 join('') 合并成一个字符串
ul.innerHTML = listItems.join('');

我们来分开解释一下map()和join()的作用

🔁 map() 之后变成了什么样?

map就是遍历每个数组元素然后通过回调函数修改内容返回,上述经过map()后其实就是返回以下内容这里的 map() 把原来的 JSON 数据(对象)转换成了 HTML 字符串。

[
    `
    <li>
        小娄 - 
        <i>抚州</i>
    </li>
    `,
    `
    <li>
        小王 - 
        <i>上饶</i>
    </li>
    `,
    `
    <li>
        小刘 - 
        <i>赣州</i>
    </li>
    `
]

🔗 join('') 之后变成什么样?

因为我们传的是空字符串 '',所以相当于把所有元素直接连在一起,不加任何分隔符。

🧵 拼接后的结果如下:

<li>
    小娄 - 
    <i>抚州</i>
</li>

<li>
    小王 - 
    <i>上饶</i>
</li>

<li>
    小刘 - 
    <i>赣州</i>
</li>

📌 总结一下流程:

步骤内容类型
原始数据JSON 数组Array<Object>
map() 后字符串数组Array<String>
join('') 后单个 HTML 字符串String
最终赋值给 innerHTML插入到 DOM 中显示为列表渲染 HTML

💡 小贴士:为什么不用 forEach?

你可能会问:我也可以用 forEach 来拼接字符串啊,干嘛非要 map + join

✅ 答案是:map() 更具语义化、更优雅、更函数式!

  • map() 表达的是“把 A 转成 B”,非常清晰;
  • forEach 更像是过程式编程,需要手动拼接字符串,不够直观;
  • map() 返回数组,方便后续处理(比如过滤、排序等);
  • join() 又快又简洁,效率更高。

三、组合技 —— 模板字符串 + map() 的完美搭档

既然我们已经掌握了字符串模板和 map(),那不妨来点高级玩法,看看它们如何配合得更好

3.1 案例一:生成带序号的列表

const fruits = ['苹果', '香蕉', '橙子'];

const list = fruits.map((fruit, index) => `
    <li>${index + 1}. ${fruit}</li>
`).join('');

document.getElementById('fruits').innerHTML = list;

在这个例子中,我们利用了 map() 的第二个参数 index,实现了自动编号的效果。

3.2 案例二:条件判断 + 模板字符串

有时候我们需要根据某些条件决定是否显示某个字段:

const users = [
    { name: 'Tom', isAdmin: true },
    { name: 'Jerry', isAdmin: false }
];

const userList = users.map(user => `
    <div class="user">
        <strong>${user.name}</strong>
        ${user.isAdmin ? '<span style="color:red">(管理员)</span>' : ''}
    </div>
`).join('');

document.getElementById('users').innerHTML = userList;

这里我们使用了三元运算符,在模板字符串内部实现了条件判断,非常灵活,判断user.isAdmin是否存在,是不是能感受到它的魅力了呢

四、常见误区 & 最佳实践

4.1 不要用 innerHTML 滥用

虽然 innerHTML 很方便,但它也有风险,尤其是在用户输入未经过滤的情况下直接插入 HTML,可能会导致 XSS 攻击。因此建议:

  • 如果只是插入纯文本,优先使用 textContent
  • 如果必须使用 HTML,务必对内容进行转义或过滤。

本人之前学过一段时间的网络安全,深知xss带来的危害

XSS(Cross-Site Scripting)是一种网络安全漏洞,攻击者通过在网页中注入恶意脚本,欺骗用户的浏览器执行该脚本,从而执行未经授权的操作。XSS 攻击通常用于窃取用户的 cookie、会话令牌或其他敏感信息,甚至可以进行钓鱼攻击。

XSS的类型

XSS 主要分为三种类型:

存储型 XSS(Persistent XSS or Stored XSS):

攻击方式: 攻击者将恶意脚本永久地存储在目标网站的数据库、文件或服务器上。 例如,攻击者可能在评论区、论坛帖子或用户个人资料中提交包含恶意脚本的内容。

攻击过程: 当其他用户访问包含恶意脚本的页面时,脚本会被从服务器加载并执行。

危害: 危害最大,影响范围广,每次访问受影响页面的用户都会受到攻击。

示例: 一个评论系统,攻击者提交包含 的评论。所有浏览该评论的用户都会看到警告框。

反射型 XSS(Reflected XSS or Non-Persistent XSS):

攻击方式: 攻击者通过 URL 参数、表单提交等方式将恶意脚本注入到用户的请求中。服务器接收到请求后,在响应中将恶意脚本反射给用户。

攻击过程: 用户需要点击包含恶意脚本的链接或提交包含恶意脚本的表单,才能触发攻击。

危害: 危害相对较小,需要用户交互才能触发,但攻击者可以通过社会工程学手段诱骗用户点击。

示例: 一个搜索功能,攻击者构造包含 的搜索链接并发送给用户。用户点击该链接后,服务器将恶意脚本返回给用户浏览器执行。

DOM 型 XSS(DOM-based XSS):

攻击方式: 攻击者通过操作页面的 DOM (Document Object Model) 来注入恶意脚本。 恶意脚本并不需要经过服务器,而是在用户的浏览器中直接修改页面的 DOM 结构。

攻击过程: 恶意脚本通常存在于 URL 的片段标识符 (hash, #) 或查询参数中,然后通过 JavaScript 代码读取这些参数并修改 DOM 结构。

危害: 危害与反射型 XSS 类似,需要用户交互才能触发。

示例: 一个 JavaScript 代码读取 URL 的 hash 值,并将其作为 HTML 内容添加到页面中。攻击者可以构造包含恶意脚本的 hash 值,例如 #,从而注入恶意脚本。

这里以本人做过的CTF题目来详细的查看XSS的危害

到一个输入界面我们先测试有没有XSS漏洞 image.png

我们来测试一下

image.png

确实存在xss的漏洞

image.png

这时候攻击者可以利用xss平台来进行攻击

会采用xss平台生成的payload来注入到我们的网站,从而可以监控我们的网站

image.png

一但有人访问,就会被xss平台所检测

image.png

这样你就被其监控了,可以获取你的cookie,会话令牌或其他敏感信息 image.png

4.2 注意性能问题

频繁操作 DOM 会带来性能损耗。虽然上面的例子都是单次更新,但如果是在循环中频繁修改 DOM,比如:

for (let i = 0; i < 1000; i++) {
    ul.innerHTML += `<li>Item ${i}</li>`;
}

这种写法会导致多次重绘重排,影响性能。正确的做法是先构建完整字符串,再一次性插入 DOM。

4.3 始终保持语义化和模块化

随着项目规模增大,建议将字符串模板和数据处理封装成函数或组件,提高复用性和可维护性:

function renderUserCard(user) {
    return `
        <div class="card">
            <h3>${user.name}</h3>
            <p>家乡:${user.hometown}</p>
        </div>
    `;
}

function renderUsers(users) {
    return users.map(renderUserCard).join('');
}

结语:拥抱 ES6,让代码更有“人情味”

JavaScript 发展至今,已经从一门“玩具语言”成长为现代前端开发的核心力量。而 ES6 的出现,则让我们真正体会到了编程的优雅与乐趣。

字符串模板和 map() 方法,就像是一对默契的搭档,一个负责优雅地拼装内容,一个负责智能地处理数据。它们不仅提升了代码的可读性,也让我们的开发效率大大增强。

下次当你面对一堆乱七八糟的字符串拼接代码时,不妨试试这两个工具,相信我,你会爱上这种“丝滑”的感觉

作者寄语:
技术的本质是解决问题,而优秀的工具可以让问题变得更容易解决。愿你在编程的路上越走越远,写出既优雅又有灵魂的代码!