2023.05.16 更新前端面试问题总结(17道题)

2023.05.05 - 2023.05.16 更新前端面试问题总结(17道题)
获取更多面试相关问题可以访问
github 地址: https://github.com/pro-collection/interview-question/issuesgitee 地址: https://gitee.com/yanleweb/interview-question/issues

目录:

  • 初级开发者相关问题【共计 5 道题】

    • 342.Grid 布局了解多少?【热度: 653】【CSS】【出题公司: 美团】
    • 347.Javascript 数组中有那些方法可以改变自身,那些不可以【JavaScript】
    • 352.HTML5 中 meta 标签作用是啥【热度: 1,562】【浏览器】【出题公司: 阿里巴巴】
    • 353.CSS 选择器有哪些、优先级如何?【热度: 1,183】【CSS】【出题公司: 腾讯】
    • 354.CSS 伪类和伪元素有哪些,它们的区别和实际应用【热度: 317】【CSS】【出题公司: 京东】
  • 中级开发者相关问题【共计 5 道题】

    • 344.[React] createPortal 了解多少?【热度: 597】【web框架】【出题公司: 网易】
    • 345.eval 了解多少?【热度: 538】【JavaScript】
    • 349.[React] memo 和 useMemo 有和区别?【热度: 654】【web框架】
    • 355.如何防止 CSS 阻塞渲染【热度: 213】【CSS】【出题公司: 网易】
    • 356.CSS 绘制三角形【热度: 324】【CSS】【出题公司: 小米】
  • 高级开发者相关问题【共计 7 道题】

    • 341.npm script 生命周期有哪些?【热度: 519】【工程化】【出题公司: 小米】
    • 343.[React] createElement 和 cloneElement 有什么区别【热度: 218】【web框架】【出题公司: 美团】
    • 346.new Function 了解多少?【热度: 1,042】【JavaScript】
    • 350.如何做 promise 缓存?上一次调用函数的 promise 没有返回, 那么下一次调用函数依然返回上一个 promise【JavaScript】
    • 351.前端水印了解多少?【热度: 641】【web应用场景】【出题公司: 小米】
    • 357.如何将JavaScript代码解析成抽象语法树(AST)【热度: 1,169】【工程化】【出题公司: 阿里巴巴】
    • 358.base64 的编码原理是什么【web应用场景】【出题公司: 腾讯】
  • 初级开发者相关问题【共计 5 道题】

    342.Grid 布局了解多少?【热度: 653】【CSS】【出题公司: 美团】

    关键词:Grid 布局、Grid 属性

    什么是 grid 布局

    CSS Grid 布局是 CSS 中的一种新的布局系统,旨在通过 网格(grid)和 行(row)、列(column)的概念来创建灵活的、高效的、响应式网页布局。CSS Grid布局可以将一个元素的内容划分为多个网格,根据需要,可以在这些网格中定位元素。与传统的基于盒子模型的布局方式不同,CSS Grid 布局以一种更直观、更高效的方式来处理布局问题。

    可以通过 CSS Grid 属性来定义网格和元素的位置,包括大小、间距、对齐方式等等。CSS Grid 布局还支持类似 Flexbox 的弹性布局,例如自适应尺寸、重叠和层叠等特性。最重要的是,因为 CSS Grid布局与内容的结构分离,所以它能够为设计响应式布局提供出色的支持,而不需要在内容标记中添加过多的 CSS 或者 JavaScript。

    grid 布局有哪些 api

    CSS Grid 布局提供了一系列的 API 来实现网格布局,以下是常用的几个属性:

  • display: grid;:设置一个元素为网格容器
  • grid-template-columns:定义网格中每一列的大小和数量
  • grid-template-rows:定义网格中每一行的大小和数量
  • grid-template-areas:为网格中的区域命名,以便将子元素分配到特定的区域
  • grid-column-gapgrid-row-gap:定义网格中行和列之间的间距
  • grid-area:定义元素应该在网格中的哪个区域,比如指定其所在的行、列和跨越的行列数量
  • grid-column-startgrid-column-end:定义元素开始和结束的列位置,类似地,grid-row-startgrid-row-end 定义元素开始和结束的行位置
  • grid-columngrid-row:简写属性,组合了 grid-column-startgrid-column-endgrid-row-startgrid-row-end,用于同时设置元素在网格中的列和行位置。
  • 这些属性可以帮助我们在网格容器中定义网格,并指定子元素在网格中的位置和大小。还有其他的属性可以进一步调整子元素的位置和大小,如 justify-selfalign-self
    用于调整子元素的水平和垂直对齐方式,grid-auto-columnsgrid-auto-rows 用于指定未被显式指定的网格单元格的大小等等。

    如何使用 grid 布局

    CSS Grid 布局可以通过以下步骤来使用:

  • 在父级元素上声明 display: grid 属性,将其转换为网格容器。
  • 使用 grid-template-columnsgrid-template-rows 属性来定义行和列的网格大小和数量,或者使用 grid-template-areas 属性来定义网格中的区域。
  • 使用 gap 属性来定义行和列之间的间距。
  • 将子元素放到网格容器中,并使用 grid-columngrid-row 属性来指定子元素在网格中的位置,也可以通过 grid-area 属性来指定子元素在网格中的区域。
  • 可以使用其他属性来进一步改变子元素的位置和大小,比如 justify-selfalign-self 等属性来设置元素的对齐方式和位置。
  • 下面是一个简单的使用 Grid 布局的示例,创建一个 3x3 网格:

    <div class="grid-container"> <div class="grid-item item1">1</div> <div class="grid-item item2">2</div> <div class="grid-item item3">3</div> <div class="grid-item item4">4</div> <div class="grid-item item5">5</div> <div class="grid-item item6">6</div> <div class="grid-item item7">7</div> <div class="grid-item item8">8</div> <div class="grid-item item9">9</div></div>
    .grid-container { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); gap: 10px;}.grid-item { background-color: #ddd; padding: 20px; font-size: 30px; text-align: center;}.item1 { grid-column: 1 / span 2; grid-row: 1;}.item2 { grid-column: 3; grid-row: 1 / span 2;}.item3 { grid-column: 1; grid-row: 2 / span 2;}.item4 { grid-column: 2; grid-row: 2;}.item5 { grid-column: 3; grid-row: 3;}.item6 { grid-column: 2 / span 2; grid-row: 4;}.item7 { grid-column: 1; grid-row: 5;}.item8 { grid-column: 2; grid-row: 5;}.item9 { grid-column: 3; grid-row: 5;}

    在这个示例中,我们创建了一个包含 9 个子元素的网格容器。通过设置网格容器的 grid-template-columnsgrid-template-rows 属性,我们定义了一个 3x3 的网格,并通过 gap
    属性设置了行和列的间距。然后,我们使用 grid-columngrid-row 属性来指定每个子元素在网格中的位置,或使用 grid-area属性来指定子元素在网格中的区域。通过这些属性的值,我们可以指定每个子元素跨越多少行和多少列,或者指定一个子元素占据网格中的多个区域。

    347.Javascript 数组中有那些方法可以改变自身,那些不可以【JavaScript】

    可以改变自身的数组方法:

  • pop(): 删除数组最后一项,并返回删除项的值。
  • push(): 向数组末尾添加一个或多个元素,并返回新数组的长度。
  • reverse(): 反转数组的顺序,返回逆序后的原数组。
  • shift(): 删除数组第一项,并返回删除项的值。
  • sort(): 对数组进行排序,返回排序后的原数组。
  • splice(): 添加或删除数组元素,返回由被删除元素组成的数组。
  • 不可以改变自身的数组方法:

  • concat(): 连接一或多个数组,返回新的合并的数组。
  • filter(): 对数组筛选符合条件的项,并返回符合条件的项组成的新数组。
  • map(): 对数组的每一项进行操作,并返回每个操作后的项组成的新数组。
  • slice(): 返回数组的一部分作为新数组,原数组不会改变。
  • join(): 将数组的每一项拼接起来作为一个字符串返回,原数组不会改变。
  • 352.HTML5 中 meta 标签作用是啥【热度: 1,562】【浏览器】【出题公司: 阿里巴巴】

    关键词:html5 meta

    HTML 5 中的 meta 标签是一个非常常用的标签,它可以用来描述一个 HTML 文档的一些基本信息与配置,包括字符编码、页面关键词、作者、视口大小等。具体来说,meta 标签可用于以下几个方面:

    1.描述文档内容:通过设置 meta 标签中的一些属性,可以描述文档的主体内容、作者、关键词和摘要等信息,以便搜索引擎索引和显示。

    2.控制页面行为:指定 meta 标签中的属性值可以控制页面的默认行为,比如设置视口大小可以实现响应式设计。

    3.声明字符编码:通过设置 meta 标签中的 charset 属性值,可以声明文档中使用的字符编码格式,帮助浏览器正确地解读页面内容。

    4.防止 XSS 攻击:设置 meta 标签的 http-equiv 属性为 content-security-policy,可以提高页面的安全性,保护页面免受跨站脚本攻击(XSS)。

    5.提供缓存机制:设置一些 meta 标签属性(如cache-control、expires、pragma),可以控制浏览器缓存页面内容的时间和方式。

    353.CSS 选择器有哪些、优先级如何?【热度: 1,183】【CSS】【出题公司: 腾讯】

    关键词:css 选择器、css 优先级

    CSS 选择器有以下几种:

    1.元素选择器:通过标签名选择元素,例如:p {}

    2.类选择器:通过 .+类名的形式选择元素,例如:.my-class {}

    3.ID 选择器:通过 #+ID名的形式选择元素,例如:#my-id {}

    4.通配符选择器:通过 * 选择所有元素,例如:* {}

    5.后代选择器:通过空格 选择某元素下的后代元素,例如:.my-parent .my-child {}

    6.子元素选择器:通过 > 选择某元素的子元素,例如:ul > li {}

    7.相邻兄弟选择器:通过 + 选择相邻的后续同级元素,例如:.my-class + p {}

    8.通用兄弟选择器:通过 ~ 选择后继的同级元素,例如:.my-class ~ p {}

    CSS 选择器的优先级从高到低如下:

  • !important:使用该关键词的属性优先级最高。

  • 行内样式:使用元素的 style 属性设置的样式优先级最高。

  • ID 选择器:指定 ID 的样式优先级高于类选择器和元素选择器。

  • 类选择器和属性选择器:优先级相同。

  • 元素选择器和伪类选择器:优先级相同。

  • 通配符和组合选择器:在没有更具体的选择器时优先级最低。

  • 需要注意,当优先级相同时,后面生效的样式会覆盖前面的样式。针对这种情况,我们可以通过提高选择器的优先级、使用 !important、使用行内样式等方式进行调整。

    354.CSS 伪类和伪元素有哪些,它们的区别和实际应用【热度: 317】【CSS】【出题公司: 京东】

    关键词:css 伪类、css 伪元素

    CSS 中有伪类和伪元素两种,它们在用法和意义上有一些区别。

    伪类是对元素在特定状态下的一种描述。比如 :hover 代表鼠标悬停状态下的样式,:active 代表元素被激活状态下的样式。伪类始终以冒号 : 开头,并放在选择器的最后。常用的 CSS 伪类有:

  • :hover,鼠标移动到元素上时产生的效果。

  • :active,鼠标按下去但没有释放时的状态。

  • :focus,元素获取焦点时的状态。

  • :visited,链接被点击并访问过时的状态。

  • :nth-child(n),选中元素的第 n 个 child 元素。

  • :first-child,选中第一个 child 元素。

  • :last-child,选中最后一个 child 元素。

  • 另一方面,伪元素是对元素局部样式的描述,允许我们对某个元素的特定部分进行样式设置。比如 ::before 可使用内容插入做出类似插画的效果,::after 可用于为元素添加内容等等。双冒号 :: 也是伪元素的标识符。常用的CSS 伪元素有:

  • ::before,在元素内容前插入内容。

  • ::after,在元素内容后插入内容。

  • ::first-letter,选择元素的第一个字母。

  • ::first-line,选择元素的第一行。

  • ::selection,选择用户选中文本的部分。

  • 区别与使用:

  • 伪类的作用是改变元素在特定状态下的样式,而伪元素则充当一个元素的某一部分来做样式处理。
  • 由于伪元素技术强大,可以为元素添加完全独立的内容而无需改变 HTML,因此在一些需要前端动态处理或给传统HTML元素嵌入样式的情况下,往往会用到伪元素技术。比如用 ::before::after 实现类似插画的效果。
  • 伪类和伪元素在实际应用中搭配使用,可以产生更复杂和丰富的样式效果。因此在大量的开发工程中,两者的灵活应用至关重要。
  • 中级开发者相关问题【共计 5 道题】

    344.[React] createPortal 了解多少?【热度: 597】【web框架】【出题公司: 网易】

    关键词:react createPortal

    createPortal 是 React 中一个用于将子元素渲染到指定 DOM 元素下的 API。

    在 React 应用中,通常会通过组件树传递 props、状态等数据来渲染 UI,并由 React 自动管理 DOM 元素的创建、更新和销毁等操作。不过,有时我们需要将某些 UI 元素渲染到根节点之外的 DOM
    元素下,例如弹出框、模态框等。这时,createPortal 就能派上用场了。

    createPortal 的用法如下:

    ReactDOM.createPortal(child, container)

    其中,child 是指要渲染的子元素,可以是任何有效的 React 元素,包括组件、HTML 元素等等;container 是指要将子元素渲染到的 DOM 元素,可以是一个有效的 DOM
    元素对象,例如通过 document.getElementById 获取到的 DOM 元素。createPortal 会将 child 渲染到 container 中,但仍然能够受到 React
    生命周期的管理,例如 componentDidMountcomponentWillUnmount 等方法。

    下面是一个例子,它展示了如何使用 createPortal 来将一个弹出框渲染到根节点之外的 DOM 元素下:

    function Dialog(props) { return ReactDOM.createPortal( <div className="dialog"> <h2>{props.title}</h2> <div>{props.content}</div> </div>, document.getElementById('dialog-container') );}function App() { return ( <div> <p>这是一个文本内容。</p> <Dialog title="提示" content="这是一个弹出框。" /> </div> );}ReactDOM.render(<App />, document.getElementById('root'));

    在这个例子中,Dialog 组件使用 createPortal 将其子元素渲染到 #dialog-container 这个元素下,而不是直接渲染到 #root 下。这个功能使得我们可以在 React应用中方便地处理弹出框等类似需求。

    345.eval 了解多少?【热度: 538】【JavaScript】

    关键词:eval 使用场景、eval 性能、eval 优点、eval 缺点

    什么是 eval

    eval() 是 JavaScript 的一个全局函数,用于解析并执行字符串代码。

    它接受一个字符串参数,该字符串包含 JavaScript 表达式或语句。在 eval 函数执行期间,该字符串的内容将被视为有效 JavaScript 代码,并运行当前作用域中的变量和函数。eval() 函数返回执行结果的值。

    举个例子:

    let x = 1;let y = 2;const result = eval("x + y"); // 将字符串作为代码执行console.log(result); // 输出 3

    eval() 常被认为是一个危险的函数,原因是它可以执行任何字符串。如果 eval() 执行了用户输入的文本,攻击者可能会注入恶意代码,从而窃取敏感信息或操纵应用程序。因此,最好不要在程序中使用 eval()函数,除非你非常明确及了解其潜在风险。

    除了 eval(),JavaScript 还提供了其他如 Function() 构造函数或 setTimeout() 等能够执行字符代码的方法,但它们的使用都需要非常小心。

    eval 的性能为何比静态编写和编译的代码要慢

    eval() 函数解析并执行动态的字符串代码,因此在运行时需要进行代码分析和编译。每次调用 eval() 都需要重复执行这些操作,这对性能的影响非常大。同时,由于 eval()执行的代码是字符串形式并不是预编译的机器代码,在执行时可能需要使用更多的内存和 CPU 资源。

    相比之下,静态编写的代码在编译时已经被转化为机器代码,因此执行速度会更快。编译器可以进行多项优化,例如移除无用的代码,减少内存分配等。这些优化在运行时是不可能完成的,因此 eval() 函数的性能相对较低。

    eval 性能一定就很差吗

    不是所有情况下 eval() 函数的性能都很差。在某些情况下,eval() 的性能可能与静态编写的代码相当。例如,如果动态代码比较简单,并且在程序运行期间只会执行一次,那么使用 eval()
    不会造成显著的性能损失。但是如果动态代码比较复杂,并且需要经常执行,那么使用 eval() 的性能就会显著低于静态编写的代码。

    另外,eval() 的性能问题还取决于运行时环境的不同。在某些浏览器中,使用 eval() 时会导致缓慢的 JavaScript
    执行,而在其他浏览器中则表现良好。因此,在编写代码时,应该始终将性能作为一个重要的因素进行考虑,并根据实际情况来选择使用 eval() 或其他适当的解决方案。

    eval 有什么优势

    eval() 函数有以下几个优势:

  • 动态执行代码:eval() 函数可以动态地将字符串解析为 JavaScript 代码并执行,从而可以在运行时动态生成代码并执行。这种动态性使得 eval()函数在一些特定的编程场景中非常有用,例如动态计算表达式、动态生成函数等。

  • 灵活性高:由于 eval() 函数可以动态解析字符串并执行其中的 JavaScript 代码,因此可以根据需要在运行时动态生成代码,而不必在编写代码时预先定义。这种灵活性使得 eval()函数在一些需要动态生成代码的场景中非常有用。

  • 命名空间:由于 eval() 函数会执行其中的 JavaScript 代码,因此代码可以利用当前作用域中的变量和函数,从而可以有效地利用命名空间并提高代码的复用性。

  • 缺点

    虽然 eval() 函数具有上述优势,但它也存在潜在的安全隐患,因此应当避免在应用程序中过度使用 eval() 函数,并在使用时注重安全性和可控性。

    349.[React] memo 和 useMemo 有和区别?【热度: 654】【web框架】

    关键词:memo useMemo

    React.memouseMemo 是在 React 中处理性能优化的两个工具,虽然它们名称相似,但是它们的作用和使用方法是不同的。

    React.memo 是高阶组件,它可以用来优化函数组件的渲染性能。它会比较当前组件的 propsstate 是否发生了变化,如果都没有变化,就不会重新渲染该组件,而是直接使用之前的结果。例如:

    import React from 'react';const MyComponent = React.memo(props => { return <div>{props.value}</div>;});

    在上面的代码中,React.memo 包装了一个简单的函数组件 MyComponent。如果该组件的 value prop 和 state 没有发生变化,那么就会直接使用之前的结果不会重新渲染。

    useMemoReact 中一个 hooks,它可以用来缓存计算结果,从而优化组件渲染性能。它接受两个参数:要缓存的计算函数和依赖项数组。每当依赖项发生变化时,该计算函数就会重新计算,并返回一个新的结果。例如:

    import React, { useMemo } from 'react';const MyComponent = props => { const result = useMemo(() => expensiveComputation(props.value), [props.value]); return <div>{result}</div>;};

    在上面的代码中,我们传递了一个计算函数 expensiveComputation,以及一个依赖项数组 [props.value]。如果依赖项没有发生变化,myValue 就会被缓存起来,不会重新计算。

    总的来说:

    React.memo 的作用是优化函数组件的渲染性能,它可以比较组件的 propsstate 是否发生变化,如果没有变化,就不会重新渲染。

    useMemo 的作用是缓存计算结果,从而避免重复计算,它接受一个计算函数和一个依赖项数组,当依赖项发生变化时,计算函数就会重新计算,返回一个新的结果,否则就会使用之前的缓存结果。

    355.如何防止 CSS 阻塞渲染【热度: 213】【CSS】【出题公司: 网易】

    关键词:css 阻塞渲染、css 阻塞

    当浏览器遇到一个 <link> 标签时,它会停止解析 HTML 并发出一个单独的网络请求去加载外部样式表。 这意味着,如果样式表很大或者网络速度很慢,它将阻止页面的渲染。阻止 CSS渲染可能会导致页面看起来很糟糕,用户无法立即看到页面内容。

    有一些方法可以防止或减轻 CSS 阻塞渲染:

  • 内联样式:使用内联样式而不是外部样式表,将样式放在页面的顶部,这样 HTML 就能很快地被渲染出来。

  • 通过媒体查询加载符合指定媒体类型或条件的样式表。这样不会影响未满足条件的设备或屏幕渲染结果。

  • 使用 rel="preload" 或者 rel="prefetch" 预加载样式表,这有助于在页面渲染过程中尽早加载样式表,提高页面加载速度。

  • 通过使用 JavaScript 动态加载样式表,可以实现延迟加载和异步加载。这可以帮助查看者能够看到尽快的内容,然后在不影响查看体验的前提下加载样式表。

  • 考虑压缩和优化您的 CSS 文件,以使代码更加紧凑、加载更快。

  • 对已经被加载的字体和图片,使用 CSS Sprites 技术合并到一个文件或者减少 HTTP 请求数量。

  • 356.CSS 绘制三角形【热度: 324】【CSS】【出题公司: 小米】

    关键词:css 绘制、css 三角形

    在CSS中,你可以使用多种方法来实现三角形。以下是几种常用的方法和相应的代码示例:

  • 使用边框:
  • .triangle { width: 0; height: 0; border-left: 50px solid transparent; border-right: 50px solid transparent; border-bottom: 100px solid red;}

    这个方法通过设置元素的边框来创建三角形,其中左右边框设为透明,底边框设置为你想要的颜色。

  • 使用伪元素:
  • .triangle { position: relative; width: 100px; height: 100px;}.triangle:before { content: ""; position: absolute; top: 0; left: 0; border-width: 0 100px 100px 0; border-style: solid; border-color: red;}

    这个方法使用伪元素 ::before 来创建三角形,通过设置其边框的宽度和样式来实现。

  • 使用旋转:
  • .triangle { width: 100px; height: 100px; background-color: red; transform: rotate(45deg);}

    这个方法创建一个正方形元素,然后通过使用 transform 属性的 rotate 函数将其旋转45度,从而形成一个三角形。

    高级开发者相关问题【共计 7 道题】

    341.npm script 生命周期有哪些?【热度: 519】【工程化】【出题公司: 小米】

    关键词:npm 生命周期、script 生命周期

    安装和卸载

    脚本名称阶段描述执行时机preinstallpre在 npm install 执行前运行,用于执行一些安装前的准备工作,例如检查依赖项或设置环境变量。安装前install, postinstallinstall在模块安装后执行,通常用于构建项目或者为其生成某些必须的文件,例如安装完毕后自动编译 TypeScript、ES6 等。安装后preuninstallpre在 npm uninstall 执行前运行,用于执行一些卸载前的准备工作。卸载前uninstallpost在 npm uninstall 执行后运行,用于清理卸载后的一些操作。卸载后postuninstallpost在 npm uninstall 执行后运行,用于执行一些卸载后的操作。卸载后

    发布和更新版本

    脚本名称阶段描述执行时机prepublishpre在 publish(npm发布)执行前,运行 npm pack。发布前preparepre在包被发布前或安装前执行,可以用来设置编译或验证文件的操作。发布前、安装前prepublishOnlypre在 npm publish 执行前运行,用于确保在 publish 命令执行时不会意外发布不必要的文件。发布前prepackpre在 npm pack(打包命令)执行前运行,用于执行一些打包前的准备工作。打包前postpackpost在 npm pack 执行后运行,用于清理和重置打包相关的操作。打包后publishpost在包被成功发布后执行。发布后postpublishpost在包被成功发布后执行,用于执行一些发布后的操作。发布后preversionpre在项目版本号更新(npm version)之前执行。更新版本号前versionpost在 npm version 执行后执行,用于执行一些版本更新后的操作。更新版本号后postversionpost在项目版本号更新(npm version)之后执行。更新版本号后

    测试和运行

    脚本名称阶段描述执行时机pretestpre在 npm test 执行前执行,用于执行某些测试前的准备工作。测试前testtest执行 npm run test 命令时执行。通常用于执行单元测试,并返回任何错误状态。默认测试阶段posttestpost在 npm test 执行后执行,用于执行某些测试后的操作。测试后prestartpre在 npm start 执行前运行,用于执行某些启动进程前的准备工作。启动前startstart执行 npm start 命令时执行,通常用于启动 Web 服务器、Node 服务器、实时编译器等。默认启动阶段poststartpost在 npm start 执行后执行,用于执行某些启动进程后的操作。启动后prerestartpre在 npm restart 执行前执行,用于执行一些重新启动进程前的准备工作。重新启动前restartstop/start执行 npm restart 命令时执行,通常用于停止正在运行的 Node 服务器、Web 服务器等,然后以更新的源码重新启动服务。默认重新启动阶段,但是该命令会触发停止和启动两个标准阶段postrestartpost在 npm restart 执行后执行,用于执行一些重新启动进程后的操作。重新启动后

    其他生命周期

    脚本名称阶段描述执行时机prestoppre在 npm stop 执行前运行,用于执行某些停止进程前的准备工作。停止前stopstop执行 npm stop 命令时执行,通常用于停止正在运行的 Web 服务器、Node 服务器、实时编译器等。默认停止阶段poststoppost在 npm stop 执行后执行,用于执行某些停止进程后的操作。停止后

    343.[React] createElement 和 cloneElement 有什么区别【热度: 218】【web框架】【出题公司: 美团】

    关键词:createElement cloneElement

    createElementcloneElement 有什么区别?

    React 中的 createElementcloneElement 都可以用来创建元素,但它们用法有所不同。

    createElement 用于在 React 中动态地创建一个新的元素,并返回一个 React 元素对象。它的用法如下:

    React.createElement(type, [props], [...children]);

    其中,type 是指要创建的元素的类型,可以是一个 HTML 标签名(如 divspan 等),也可以是一个 React 组件类(如自定义的组件),props 是一个包含该元素需要设置的属性信息的对象,children
    是一个包含其子元素的数组。createElement 会以这些参数为基础创建并返回一个 React 元素对象,React 将使用它来构建真正的 DOM 元素。

    cloneElement 用于复制一个已有的元素,并返回一个新的 React 元素,同时可以修改它的一些属性。它的用法如下:

    React.cloneElement(element, [props], [...children]);

    其中,element 是指要复制的 React 元素对象,props 是一个包含需要覆盖或添加的属性的对象,children 是一个包含其修改后的子元素的数组。cloneElement会以这些参数为基础复制该元素,并返回一个新的 React 元素对象。

    在实际使用中,createElement 通常用于创建新的元素(如动态生成列表),而 cloneElement 更适用于用于修改已有的元素,例如在一个组件内部使用 cloneElement 来修改传递进来的子组件的属性。

    cloneElement 有哪些应用场景

    React 中的 cloneElement 主要适用于以下场景:

  • 修改 props
  • cloneElement 可以用于复制一个已有的元素并覆盖或添加一些新的属性。例如,可以复制一个带有默认属性的组件并传递新的属性,达到修改属性的目的。

    // 假设有这样一个组件function MyComponent(props) { // ...}// 在另一个组件中使用 cloneElement 修改 MyComponent 的 propsfunction AnotherComponent() { return React.cloneElement(<MyComponent />, { color: 'red' });}

  • 渲染列表
  • 在渲染列表时,可以使用 Array.map() 生成一系列的元素数组,也可以使用 React.Children.map() 遍历子元素并返回一系列的元素数组,同时使用 cloneElement 复制元素并传入新的 key 和props。

    // 使用 Children.map() 遍历子元素并复制元素function MyList({ children, color }) { return ( <ul> {React.Children.map(children, (child, index) => React.cloneElement(child, { key: index, color }) )} </ul> );}// 在组件中使用 MyList 渲染列表元素function MyPage() { return ( <MyList color="red"> <li>Item 1</li> <li>Item 2</li> <li>Item 3</li> </MyList> );}

  • 修改子元素
  • 使用 cloneElement 也可以在一个组件内部修改传递进来的子组件的属性,例如修改按钮的样式。

    function ButtonGroup({ children, style }) { return ( <div style={style}> {React.Children.map(children, (child) => React.cloneElement(child, { style: { color: 'red' } }) )} </div> );}function MyPage() { return ( <ButtonGroup style={{ display: 'flex' }}> <button>Save</button> <button>Cancel</button> </ButtonGroup> );}

    总之,cloneElement 可以方便地复制已有的 React 元素并修改其属性,适用于许多场景,例如修改 props、渲染列表和修改子元素等。

    346.new Function 了解多少?【热度: 1,042】【JavaScript】

    关键词:new Function 概念、js new Function 执行性能、js new Function 使用场景

    基本概念

    new Function() 是 JavaScript 中的一个构造函数,它可以实例化一个新的函数对象并返回。该函数对象可以使用传递给 new Function()的字符串参数作为函数体,并使用其他传递给它的参数作为函数参数,从而动态创建一个可执行的函数。

    具体来说,new Function() 构造函数可以接受多个字符串参数作为函数的参数和函数体,其参数形式如下:

    new Function ([arg1[, arg2[, ...argn]],] functionBody)

    其中,arg1, arg2, ..., argn 为函数的参数列表,functionBody 为函数体的字符串表示。当调用 new Function() 函数时,JavaScript
    引擎会将 arg1, arg2, ..., argn 所表示的参数和 functionBody 所表示的函数体组合成一个新的函数对象,并将该对象返回。

    举例

    下面是一个简单的 new Function() 的使用示例,它使用 new Function() 构造函数动态创建一个函数对象,并将该对象作为变量 add 的值进行赋值:

    const add = new Function('a', 'b', 'return a + b;');console.log(add(2, 3)); // 5

    上述代码中,new Function('a', 'b', 'return a + b;') 创建了一个新的函数对象,其中 'a''b' 是函数的参数列表,'return a + b;'
    是函数的实现代码。然后,该函数对象被赋值给变量 add。最后,调用 add(2, 3) 执行该函数,返回 5

    需要注意的是,new Function() 构造函数不能访问其上下文中的变量和函数,因此在使用时需要特别注意作用域的限制。同时,由于 new Function()构造函数的执行权限较为灵活,因此在使用时需要仔细检查并确保其输入参数的合法性和安全性。

    new Function 和 eval 的区别

    虽然 new Function()eval() 都可以执行字符串形式的 JavaScript 代码,但是它们在执行方式、使用场景和安全性方面还是有很大的区别的。

    下面是 new Function()eval() 的主要区别:

  • 执行方式不同:new Function() 构造函数创建的函数对象只会在其被调用时才会执行,而 eval() 函数则立即执行其参数中的 JavaScript 代码,并返回其中的值(如果有)。

  • 作用域不同:new Function() 构造函数创建的函数对象没有访问父作用域的能力,只能访问自己的局部变量和全局变量;而 eval()函数则可以访问其自身函数作用域和父作用域的变量和函数,因此具有更高的灵活性和不可预知性。

  • 安全性不同:由于 new Function() 构造函数定义的函数对象是在严格的函数作用域下运行的,因此其代码不会改变或访问父作用域中的变量。因此,使用 new Function()
    构造函数创建函数对象时,可以更好地保证其安全性。而 eval() 函数则无法保证代码的安全性,因为它可以访问并改变父作用域中的变量,从而具有更高的攻击风险。

  • new Function 性能

    eval() 相比,new Function() 函数具有更好的性能。这是因为 new Function() 函数在编译时会创建一个新的函数对象,不会像 eval()
    函数一样将代码注入到当前作用域中。相反,它只在需要时才编译并执行代码,因此在常规情况下,new Function() 的性能比 eval() 更好。

    另外,由于 new Function() 在全局作用域外部定义新的函数,可以更好地掌控执行环境,因此我们可以利用 new Function() 函数的局部性,使其不仅取代 eval(),而且在一定程度上比自执行函数和即时函数表达式引入更少的全局变量。

    不过需要注意的是,如果在一个循环中频繁地使用 new Function(),或者在函数体内创建过多的嵌套函数,可能会导致性能下降。因此,当需要使用 new Function()函数时,应该尽量减少不必要的重复调用,并注意代码的优化和缓存。

    new Function 使用场景

    new Function() 的使用场景主要是动态生成 Javascript 代码的情况。由于它可以使用字符串形式作为函数体,并接受可变数量的参数,因此很适合在需要动态生成 JavaScript代码的场景中使用。下面列举一些常见的使用场景:

  • 动态生成函数:使用 new Function() 可以动态生成函数,有时候这种方式比使用函数表达式更加灵活。

  • 模板引擎:某些模板引擎使用 new Function() 动态生成 JavaScript 代码来进行文本渲染和数据绑定。

  • 解析 JSON:从服务端获取 JSON 数据时,可以使用 new Function() 将其转换为具有更好可读性的 JavaScript 对象。 举例:

  • const json = '{"name": "张三", "age": "18", "gender": "男"}';const parseJson = new Function(`return ${json}`);console.log(parseJson()); // 输出:{name: "张三", age: "18", gender: "男"}

  • 在浏览器中查找或执行某些 DOM 元素:可以将 JavaScript 代码传递给 new Function() 进行动态执行和查找。
  • 需要注意的是,由于 new Function() 可以动态生成和执行任意 JavaScript 代码,因此其安全性和风险需要仔细考虑和评估。在使用 new Function()时,应该避免用于可疑的或不可信任的代码执行,并严格控制传递给函数构造函数的参数,以避免潜在的安全漏洞。

    350.如何做 promise 缓存?上一次调用函数的 promise 没有返回, 那么下一次调用函数依然返回上一个 promise【JavaScript】

    基础实现

    可以使用闭包实现 promise 缓存的功能。下面是一个示例代码:

    function cachedPromise(promiseFunction) { let lastPromise = null; return function() { if (lastPromise !== null) { return lastPromise; } lastPromise = promiseFunction(); return lastPromise; }}const promiseFunction = () => { // 这里可以是任何一个返回 Promise 的异步函数 return new Promise(resolve => { setTimeout(() => { resolve('Resolved!'); }, 2000) })}const cachedPromiseFunction = cachedPromise(promiseFunction);cachedPromiseFunction().then(result => { console.log(result); // Resolved!});// 因为上次调用函数的 Promise 还未 resolve,所以这里直接返回上次的 PromisecachedPromiseFunction().then(result => { console.log(result); // Resolved!});

    在上面的代码中,我们先定义了一个 cachedPromise 函数,它接收一个返回 Promise 的异步函数 promiseFunction,并返回一个新的函数。这个新函数会维护一个 lastPromise
    变量,用来记录上一次调用 promiseFunction 函数返回的 Promise。

    当第一次调用 cachedPromiseFunction 时,lastPromise 变量还没有值,因此会调用 promiseFunction,并将返回的 Promise 赋值给 lastPromise 变量。同时,返回这个Promise。

    当第二次调用 cachedPromiseFunction 时,由于 lastPromise 变量已经被赋值,表示上一次调用 promiseFunction 返回的 Promise
    还没有返回,因此直接返回 lastPromise 变量,而不再调用 promiseFunction

    当第一个 Promise 返回时,会将 lastPromise 重置为空,这样下一次调用 cachedPromiseFunction 就会重新执行 promiseFunction

    通过这种方式,我们就实现了 promise 缓存的功能,即如果上一次调用的 Promise 没有返回,那么下一次调用函数依然会返回上一个 Promise。

    如果上一次的函数调用 promise 已经返回,下一次调用就是一个新的 promise

    修改上述的代码,让 cachedPromise 函数可以检测上一次的 Promise 是否已经完成,如果已经完成,则返回新的 Promise 对象。

    下面是修改后的代码:

    function cachedPromise(promiseFunction) {  let lastPromise = null;    return function() {    if (lastPromise !== null) {      if (lastPromise.isFulfilled()) { // 如果上一次 Promise 已经完成        lastPromise = null; // 重置上一次 Promise      } else {        return lastPromise; // 直接返回上一次 Promise      }    }        lastPromise = promiseFunction();    return lastPromise;  }}const promiseFunction = () => {  // 这里可以是任何一个返回 Promise 的异步函数  return new Promise(resolve => {    setTimeout(() => {      resolve('Resolved!');    }, 2000)  })}const cachedPromiseFunction = cachedPromise(promiseFunction);cachedPromiseFunction().then(result => {  console.log(result); // Resolved!});// 因为上次调用函数的 Promise 还未 resolve,所以这里直接返回上次的 PromisecachedPromiseFunction().then(result => {  console.log(result); // Resolved!});setTimeout(() => {  // 上一次 Promise 已经完成,这里会返回新的 Promise 对象  cachedPromiseFunction().then(result => {    console.log(result); // Resolved!  });}, 3000);

    在这段代码中,我们在闭包函数中判断上一次的 Promise 是否已经完成,如果已经完成,则将其重置为空,在下一次调用时会再次执行 promiseFunction,并返回新的 Promise 对象。

    请注意,由于 lastPromise 变量被修改了,我们使用了一个名为 isFulfilled() 的方法来检测 Promise 是否已经完成。这个方法可以使用任何一个符合 Promises/A+ 规范的 Promise 库(如
    bluebird.js)来实现。如果你使用的是原生的 Promise 对象,可以使用 then() 方法代替 isFulfilled(),如下所示:

    if (typeof lastPromise.then !== 'function') {  lastPromise = null; // 重置上一次 Promise} else {  return lastPromise; // 直接返回上一次 Promise}

    这样,我们就实现了一个可以检测 Promise 完成状态的 promise 缓存函数。

    351.前端水印了解多少?【热度: 641】【web应用场景】【出题公司: 小米】

    关键词:前端 明水印 暗水印

    明水印和暗水印的区别

    前端水印可以分为明水印和暗水印两种类型。它们的区别如下:

  • 明水印:明水印是通过在文本或图像上覆盖另一层图像或文字来实现的。这种水印会明显地出现在页面上,可以用来显示版权信息或其他相关信息。

  • 暗水印:暗水印是指在文本或图像中隐藏相关信息的一种技术。这种水印不会直接出现在页面上,只有在特殊的程序或工具下才能被检测到。暗水印通常用于保护敏感信息以及追踪网页内容的来源和版本。

  • 添加明水印手段有哪些

    可以参考这个文档: https://zhuanlan.zhihu.com/p/374734095

    总计一下:

  • 重复的dom元素覆盖实现: 在页面上覆盖一个position:fixed的div盒子,盒子透明度设置较低,设置pointer-events:none;样式实现点击穿透,在这个盒子内通过js循环生成小的水印div,每个水印div内展示一个要显示的水印内容

  • canvas输出背景图: 绘制出一个水印区域,将这个水印通过toDataURL方法输出为一个图片,将这个图片设置为盒子的背景图,通过backgroud-repeat:repeat;样式实现填满整个屏幕的效果。

  • svg实现背景图: 与canvas生成背景图的方法类似,只不过是生成背景图的方法换成了通过svg生成

  • 图片加水印

  • css 添加水印的方式, 如何防止用户删除对应的 css , 从而达到去除水印的目的

    使用 CSS 添加水印的方式本身并不能完全防止用户删除对应的 CSS 样式,从而删除水印。但是,可以采取一些措施来增加删除难度,提高水印的防伪能力。以下是一些常见的方法:

  • 调用外部CSS文件:将水印样式单独设置在一个CSS文件内,并通过外链的方式在网站中调用,可以避免用户通过编辑页面HTML文件或内嵌样式表的方式删除水印。

  • 设置样式为 !important:在CSS样式中使用 !important 标记可以避免被覆盖。但是,这种方式会影响网页的可读性,需慎重考虑。

  • 添加自定义类名:通过在CSS样式中加入自定义的class类名,可以防止用户直接删掉该类名,进而删除水印。但是,用户也可以通过重新定义该类名样式来替换水印。

  • 将水印样式应用到多个元素上:将水印样式应用到多个元素上,可以使得用户删除水印较为困难。例如,在网站的多个位置都加上"Power by XXX"的水印样式。

  • 使用JavaScript动态生成CSS样式:可以监听挂载水印样式的dom 节点, 如果用户改变了该 dom , 重新生成 对应的水印挂载上去即可。这种方法可通过JS动态生成CSS样式,从而避免用户直接在网页源文件中删除CSS代码。但需要注意的是,这种方案会稍稍加重网页的加载速度,需要合理权衡。

  • 混淆CSS代码:通过多次重复使用同一样式,或者采用CSS压缩等混淆手段,可以使CSS样式表变得复杂难懂,增加水印被删除的难度。

  • 采用图片水印的方式:将水印转化为一个透明的PNG图片,然后将其作为网页的背景图片,可以更有效地防止水印被删除。

  • 使用SVG图形:可以将水印作为SVG图形嵌入到网页中进行展示。由于SVG的矢量性质,这种方式可以保证水印在缩放或旋转后的清晰度,同时也增加了删除难度。

  • 暗水印是如何把水印信息隐藏起来的

    暗水印的基本原理是在原始数据(如文本、图像等)中嵌入信息,从而实现版权保护和溯源追踪等功能。暗水印把信息隐藏在源数据中,使得人眼难以察觉,同时对源数据的影响尽可能小,保持其自身的特征。

    一般来说,暗水印算法主要包括以下几个步骤:

  • 水印信息处理:将待嵌入的信息经过处理和加密后,转化为二进制数据。

  • 源数据处理:遍历源数据中的像素或二进制数据,根据特定规则对其进行调整,以此腾出空间插入水印二进制数据。

  • 嵌入水印:将水印二进制数据插入到源数据中的指定位置,以某种方式嵌入到源数据之中。

  • 提取水印:在使用暗水印的过程中,需要从带水印的数据中提取出隐藏的水印信息。提取水印需要使用特定的解密算法和提取密钥。

  • 暗水印的一个关键问题是在嵌入水印的过程中,要保证水印对源数据的伤害尽可能的小,同时嵌入水印后数据的分布、统计性质等不应发生明显变化,以更好地保持数据的质量和可视效果。

    357.如何将JavaScript代码解析成抽象语法树(AST)【热度: 1,169】【工程化】【出题公司: 阿里巴巴】

    关键词:解析为 AST、抽象语法树、AST 词法分析、AST 语法分析

    如何将JavaScript代码解析成抽象语法树

    要将JavaScript代码解析成抽象语法树(Abstract Syntax Tree,AST),你可以使用工具或库来实现。以下是几种常用的方法:

  • Esprima: Esprima 是一个流行的JavaScript解析器,它可以将JavaScript代码解析成AST。你可以使用它的 JavaScript API 来将代码解析成AST对象。
  • const esprima = require('esprima');const code = 'var x = 5;';const ast = esprima.parseScript(code);console.log(ast);

  • Acorn: Acorn 是另一个广泛使用的JavaScript解析器,它也可以将JavaScript代码解析成AST。你可以使用它的 JavaScript API 来解析代码并获取AST对象。
  • const acorn = require('acorn');const code = 'var x = 5;';const ast = acorn.parse(code, { ecmaVersion: 2020 });console.log(ast);

  • Babel: Babel 是一个功能强大的JavaScript编译器,它可以将代码转换为AST,并提供了丰富的插件系统,用于转换和操作AST。你可以使用 Babel 的 API 来解析代码并获取AST对象。
  • const babelParser = require('@babel/parser');const code = 'const x = 5;';const ast = babelParser.parse(code, { sourceType: 'module' });console.log(ast);

    这些工具和库都可以将JavaScript代码解析成AST对象,从而使你能够对代码进行进一步的分析、转换或处理。你可以根据自己的需求选择其中之一,并根据其文档了解更多关于解析选项和AST节点的信息。

    JavaScript代码解析成抽象语法树的原理是什么

    JavaScript代码解析成抽象语法树(Abstract Syntax Tree,AST)的过程涉及以下几个主要步骤:

  • 词法分析(Lexical Analysis):词法分析器(Lexer)将源代码拆分成词法单元(tokens),比如变量名、关键字、操作符、标点符号等。它根据一组定义好的规则(词法规范)来识别和分类这些词法单元。

  • 语法分析(Syntax Analysis):语法分析器(Parser)接收词法分析器生成的词法单元,并根据语法规则构建AST。语法分析器使用上下文无关文法(Context-FreeGrammar)来定义语言的语法规则,它通过递归下降、LR(1) 等算法来处理这些规则,以确定输入是否符合语法规则并生成相应的AST。

  • 构建AST:在语法分析的过程中,语法分析器根据语法规则构建AST。AST是一个树状结构,其中每个节点表示源代码中的一个语法结构,如表达式、语句、函数等。不同节点类型代表不同的语法结构,它们之间通过父子关系和兄弟关系来表示源代码的层次结构和逻辑关联。

  • 后续处理:生成AST后,可以进行进一步的处理和分析。这可能包括语义分析、类型推断、符号解析、代码优化等。这些步骤可以根据具体的需求和工具进行。
  • 总结:将JavaScript代码解析成AST的过程是通过词法分析器将源代码拆分成词法单元,然后语法分析器根据语法规则构建AST。AST提供了对代码结构的抽象表示,便于进一步分析、转换和操作代码。

    358.base64 的编码原理是什么【web应用场景】【出题公司: 腾讯】

    Base64编码是一种用于将二进制数据转换为可打印ASCII字符的编码方式。它的原理如下:

  • 将原始数据划分为连续的字节序列。

  • 将每个字节转换为8位二进制数。

  • 将这些二进制数按照6位一组进行分组,不足6位的用0补齐。

  • 将每个6位的二进制数转换为对应的十进制数。

  • 根据Base64字符表,将十进制数转换为相应的可打印ASCII字符。

  • Base64字符表由64个字符组成,通常使用以下字符:A-Z、a-z、0-9以及字符"+“和”/"。这些字符可以通过索引值与相应的十进制数进行对应。

    编码过程中,如果原始数据的长度不是3的倍数,会根据需要进行填充。填充通常使用字符"=",每个填充字符表示4位的零值。

    解码时,按照相反的过程进行操作。将Base64编码后的字符串按照4个字符一组分组,并将每个字符转换回对应的十进制数。然后将这些十进制数转换为6位二进制数,并将这些二进制数连接起来。最后,将连接后的二进制数划分为8位一组,并将每个8位二进制数转换为对应的字节数据。

    Base64编码主要应用于在文本协议中传输或存储二进制数据,例如在电子邮件中传输附件或在Web中传输图像数据。它可以将二进制数据转换为ASCII字符,使其在不支持二进制传输的环境中能够正常处理。

    ©著作权归作者所有,转载或内容合作请联系作者
    • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
      沈念sama阅读 203,937评论 6 478
    • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
      沈念sama阅读 85,503评论 2 381
    • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
      开封第一讲书人阅读 150,712评论 0 337
    • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
      开封第一讲书人阅读 54,668评论 1 276
    • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
      茶点故事阅读 63,677评论 5 366
    • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
      开封第一讲书人阅读 48,601评论 1 281
    • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
      沈念sama阅读 37,975评论 3 396
    • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
      开封第一讲书人阅读 36,637评论 0 258
    • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
      沈念sama阅读 40,881评论 1 298
    • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
      茶点故事阅读 35,621评论 2 321
    • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
      茶点故事阅读 37,710评论 1 329
    • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
      沈念sama阅读 33,387评论 4 319
    • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
      茶点故事阅读 38,971评论 3 307
    • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
      开封第一讲书人阅读 29,947评论 0 19
    • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
      开封第一讲书人阅读 31,189评论 1 260
    • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
      沈念sama阅读 44,805评论 2 349
    • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
      茶点故事阅读 42,449评论 2 342

    推荐阅读更多精彩内容