软件研发技术(前端开发)测试题
前端工程师必须掌握的简单前端开发测试题,可根据招聘研发职级的不同可选部分题目设置合理时间予以测试,持续补充ing
一、填空题
1. rem以 html 标签的字号为基准,可以通过动态设置字号,来实现页面的自适应效果。
2. 设置元素浮动后,该元素的 display 值是 block 。
绝对定位的元素其定位参考的是哪个元素 最近的已定位(相对、绝对、固定)父元素 。
3. 当声明的变量还未被初始化时,变量的默认值为 undefined , null 用来表示尚未存在的对象。
4. JavaScript中强制类型转换为number类型的方法有 Number 、 parseInt 、 parseFloat 。
5. JavaScript中多次执行的定时器为 setIntervar ,只执行一次的定时器为 setTimeout ,清除定时器的方法 clearIntervar 、clearTimeout 。
6. ES6新增了哪两个声明: let 、 const 。
7. ES6中用 class 定义类,用 extends 实现类的继承。
8. async函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。await命令只能用在 async 函数之中,如果用在普通函数,就会报错
9. ES6标准中,使用 export 对外输出本模块的变量接口,使用 import 加载另一个模块的接口。
10. Vue3.x使用 Proxy 创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)
11. 前端目前的框架有哪些 vue,react,preact 构建框架有哪些 gulp,webpack,rollup,vite,esbuild,seveltjs
12. git哪个命令可以将已暂存的文件提交到git服务器上 git push ,git查看当前修改了那些文件可以用什么命令 git status 。
二、简答题
1. 介绍一下标准的css的盒子模型?与低版本IE的盒子模型有什么不同?boxSizing:border-box||content-box;
2. 介绍一下css3 属性值var()和calc()的用法?var()引用父元素样式的—attr属性,calc参与计算。
3. li元素display设置为inline-block时,li与li之间有看不见的空白间隔是什么原因引起的?有什么解决办法?
Fontsize为0时空格,回车等空字符占位,可以设置父类font-size:0
4. 请问left和 right的宽度分别是多少?left 25px,right:175px
<document>
<style type="text/css">
.container{
width: 200px;
height: 200px;
display: flex;
flex-direction: row;
align-items: stretch;
}
.left{
flex: 1;
border: 1px solid #ccc;
}
.right{
flex: 1 0 150px;
border: 1px solid #ccc;
}
</style>
<div class="container">
<div class="left"></div>
<div class="right"></div>
</div>
</document>
5. Javascript的基础数据类型有哪些?如何检测某个数据类型属于哪一种基础数据类型?获取精确数据对象类型的通用方法是?==的比较原理是怎样的?7+1个基础数据类型 ,typeof ,Object.protoType.toString.call(obj).slice(8,-1)
6. 解释下javascript里的this是如何工作的?arrow func:运行环境上下文this,call,apply,bind声明this指向
7. 你对原型链是怎样理解的,原型链是一种什么数据结构,原型链的顶层是什么,Object.create(null)与{}有什么区别?
8. 前后端分离项目中如果前后端工程都微服务化,如何解决跨域问题,如何做服务映射?假如客户端直接发了一个跨域请求,服务端可否能接收到这个请求并反馈结果给客户端?
可以设置前端网关去解析转发。服务端设置返回头部允许跨域访问域。客户端设置服务名与域映射和转发。可以,跨域的核心在于数据安全性,客户端只作拦截。
9. 前端的本地缓存类型都有哪些,有什么区别?
localStorage,sessionStorage, document.cookie,indexDb 。大小不同,localStorage可共享且只可手动清除,sessionStorage刷新不会清除且有随页面关闭而清除。cookie可以设置过期时间有跨域限制,indexDb 客户端数据库。
10. 前端常用的数据结构有哪些? 栈,队列,树,链表(环形,交叉),字典,集合(交,并,补,差),图(无向图,有向图,加权图 ,连通图。邻接矩阵、邻接链表 ) 以及 权重的引入
11. 前端常用的设计模式有哪些?外观模式,代理模式,工厂模式,单例模式,策略模式,迭代器模式,观察者模式,中介者模式,访问者模式,装饰器模式。
12. Vue父子组件交互有哪些方式? vue有哪些钩子函数(15+)?
props,$emit,$parent,$refs等。8普通,2保持,7路由(3组件+3全局+1路由表),@hook:生命周期钩子名
13. 如下:
定义导出:
const dataA = {
a: 1,
b:[1,2]
}
export default dataA
A组件:
import dataA from 'url/*/*/*/*'
export default {
name: 'Main',
data() {
return {
dataA,
}
},
methods: {
setData() {
this.dataA.b[2]=3
}
},
mounted() {
this.setData()
}
}
(1)如上B组件也像A组件这么导入和调用,如果A组件先掉用,B组件再掉用,请问B组件导入进来的dataA是A组件修改后的值还是定义导出所写的那个值?修改后的
定义导出:
const dataA = {
a: 1,
b:[1,2]
}
export default function(){
return dataA
}
A组件:
import dataA from 'url/*/*/*/*'
export default {
name: 'Main',
data() {
return {
data: data(),
}
},
methods: {
setData() {
this.dataA.b[2]=3
}
},
mounted() {
this.setData()
}
}
(2)如上,如果对1稍作调整,此时,B组件也像A组件这么导入和调用,如果A组件先调用,B组件再调用,请问B组件导入进来的dataA是A组件修改后的值还是定义导出所写的那个值?修改后的
(3)如果要使每次导入进来的都是初始化定义的那包数据,可以怎么修改或怎么处理?导出时函数里直接返回具体的数据
14. 多个菜单共用了同一个路由组件如何对应保持和销毁?动态addRoute路由+重写以志气以路由名称和组件名称 为key的保活组件。
15. 登陆后门户进入到子模块会携带一个token过来,要求模块刚进来的时候获取地址栏的token保存在页面数据里面且把地址栏的token干掉保证页面不刷新且页面刷新的时候token有效,关闭需要token无效掉,这个怎样处理? history.pushState,replaceState, window.onunload设置cookie并设置一个过期时间或设置sessionStorage,window.onload去获取。
16. 平时在项目开发中都做过哪些前端性能优化?结合谷歌提出的性能指标(加载性能(LCP)-- 显示最大内容元素所需时间,交互性(FID)-- 首次输入延迟时间,视觉稳定性(CLS)-- 累积布局配置偏移)。开发,资源,请求,数据,缓存,贯彻整个软件过程。
17. 微服务整合? 阿里的乾坤,京东的mycro-app, 以及 未来的webComponent(customElement,shadowDom)+module(js沙箱)+Web Worker。
18. 列举不下十条好的规范建议?
19. 你是怎么理解前端以及前端的未来发展的?如果今年你打算熟练掌握一项新技术,那会是什么?
20. 你对自己综合定位是什么,未来五年的职业计划是怎样的?
三、实践题
1. 实现一个通用的判断回文数组的方法(初级)
判断回文数组
let a = [1, 2, 3, 4, 3, 2, 1] // true
let b = [1, 2, 3, 4, 4, 3, 2, 1] // true
let c= ['a', 'b', 'c', 'd', 'c','b', 'a'] // true
let d = [1, 2, 3, 4, 5, 3, 2, 1] // false
let e = [1, 2, 3, 4, 5, 6, 'a', 2] // false
function isPalindromeArr(arr) {
let middle = arr.length / 2
for (let i = 0; i <= middle; i++) {
if (arr[i] !== arr[arr.length - i - 1]) {
return false
}
}
return true
}
2. 实现一个防抖函数(初级)
// 调用
function fn() {
console.log('防抖')
}
addEventListener('scroll', debounce(fn, 1000))
function debounce(fn, delay) {
let timer = null
return function () {
let context = this
let arg = arguments
// 在规定时间内再次触发会先清除定时器后再重设定时器
clearTimeout(timer)
timer = setTimeout(function () {
fn.apply(context, arg)
}, delay)
}
}
3. 实现一个简单的双向数据绑定(初级)
let obj = {}
let input = document.getElementById('input')
Object.defineProperty(obj, 'text', {
configurable: true,
enumerable: true,
get() {
console.log('获取数据了')
},
set(newVal) {
console.log('数据更新了')
input.value = newVal
span.innerHTML = newVal
}
})
// 输入监听
input.addEventListener('keyup', function (e) {
obj.text = e.target.value
})
4. 实现一个通过回调里面return false来终止整个循环的类似于forEach这样掉用的迭代器,并且可以遍历对象,数组等(中级)
// 掉用
each([1, 2, 3], (item, index) => {
console.log(this)
// option
if (item == 2) {
retrun false
}
})
function each(object, callback) {
// 可终止循环each方法
var type = (function (obj) {
switch (obj.constructor) {
case Object:
return 'Object';
break;
case Array:
return 'Array';
break;
case NodeList:
return 'NodeList';
break;
default:
return 'null';
break;
}
})(object);
// 为数组或类数组时, 返回: index, value
if (type === 'Array' || type === 'NodeList') {
// 由于存在类数组NodeList, 所以不能直接调用every方法
[].every.call(object, function (v, i) {
return callback.call(v, v,i) === false ? false : true;
});
}
// 为对象格式时,返回:key, value
else if (type === 'Object') {
for (var i in object) {
if (callback.call(object[i], object[i], i) === false) {
break;
}
}
}
}
5. 请将下面数据nodes转换为nodesTreeSum(中级)
let nodes = [
{
a: 0,
b: 0,
itemCode: '10'
},
{
a: 0,
b: 0,
itemCode: '101001'
},
{
a: 1,
b: 3,
itemCode: '1010010001'
},
{
a: 2,
b: 3,
itemCode: '1010010002'
},
{
a: 3,
b: 7,
itemCode: '101002'
},
{
a: 2,
b: 2,
itemCode: '11'
}
]
转换得到:
let nodesTreeSum = [
{
a: 6,
b: 13,
itemCode: '10',
children: [
{
a: 3,
b: 6,
itemCode: '101001',
children: [
{
a: 1,
b: 3,
itemCode: '1010010001'
},
{
a: 2,
b: 3,
itemCode: '1010010002'
}
]
},
{
a: 3,
b: 7,
itemCode: '101002'
}
]
},
{
a: 2,
b: 2,
itemCode: '11'
}
]
const Utils={
listToTreeByNestCodeLength(data, nestCodeKey) { // 平行数据 根据nestCodeKey 长度 生成嵌套数据
let map = {} // 构建map
data.forEach(function (item) {
map[item[nestCodeKey]] = item
item.children = []
})
let treeData = []
data.forEach(item => {
let pnestCodeKey = item[nestCodeKey]
pnestCodeKey = pnestCodeKey.slice(0, pnestCodeKey.length - 1)
let mapItem = map[item[pnestCodeKey]]
while (!mapItem && pnestCodeKey !== '') {
pnestCodeKey = pnestCodeKey.slice(0, pnestCodeKey.length - 1)
mapItem = map[pnestCodeKey]
}
if (mapItem) {
(mapItem.children || (mapItem.children = [])).push(item)
} else {
treeData.push(item)
}
})
return treeData
},
performGradedSummaryData(data, gradedCalcFields) { // 数组分级汇总计算
return data.reduce((result, next) => {
this.performGradedSummaryDataSing(next, gradedCalcFields)
gradedCalcFields.forEach((key) => {
result[key] = (result[key] || 0) + next[key]
})
return result
}, {})
},
performGradedSummaryDataSing(obj, gradedCalcFields) { // 数组分级汇总单条计算
if (Array.isArray(obj.children) && obj.children.length) {
Object.assign(obj, this.performGradedSummaryData(obj.children, gradedCalcFields))
}
return obj
}
}
调用: Utils.performGradedSummaryData(Utils.listToTreeByNestCodeLength(nodes, 'itemCode'), ['a', 'b'])
6. 请实现一个通用的树形过滤方法(中级)
function filterData(data, filterMethod, leaf = true) { // 过滤树形数据
return data.map((item, index) => { return Object.assign({}, item) }).filter((item, index) => {
if (Array.isArray(item.children) && item.children.length) {
let result = filterData(item.children, filterMethod, leaf)
if (result.length) {
item.children = result
return true
} else {
if (leaf) {
return false
} else {
item.children = []
return filterMethod(item)
}
}
} else {
return filterMethod(item)
}
})
}
7. 观察并实现一个通用的方法处理转换下列数据(中级)
例如:
[7, 53, 54] // 75453;
[1, 321, 35, 4] // 4353211;
[220, 22, 34, 10000, 234] // '342342222010000'
求数组中所有数字可以拼接出的最大整数。
function toMaxInt(arr) {
let newArr = arr.sort((a, b) => {
a += '';
b += '';
let aLen = a.length, bLen = b.length;
let aIndex = 0, bIndex = 0;
while (aIndex <= aLen && bIndex < bLen) {
if (arr[aIndex] === arr[bIndex]) {
aIndex + 1 < aLen && (aIndex++, flag = true);
bIndex + 1 < bLen && (bIndex++, flag = true);
} else {
break
}
if (flag) break;
}
if (arr[aIndex] > arr[bIndex]) {
return -1;
} else {
return 1;
}
})
return newArr.join('')
}
8. 请实现Somebody.doSth().sleep().doSth().sleep().sleep().doSth()(高级)
调用:
new Somebody('tom').doSth(() => {
console.log(1)
}).sleep(1000).doSth(() => {
console.log(2)
}).sleep(2000).sleep(3000).doSth(() => {
console.log(3)
})
实现:
class Somebody {
constructor() {
this.init()
}
init() {
this.name = name
this.queue = []
console.log(`Hi I am ${name}`)
setTimeout(() => {
this.next()
}, 0)
return this
}
sleep(time) {
const fn = () => {
setTimeout(() => {
console.log(`等待了${time}秒...`)
this.next()
}, time)
}
this.queue.push(fn)
return this
}
doSth(cb) {
const fn = () => {
cb()
this.next()
}
this.queue.push(fn)
return this
}
next() {
const fn = this.queue.shift()
fn && fn()
}
}
9. 请实现一个完备的深拷贝(高级)
评价一个深拷贝是否完善,请检查以下问题是否都实现了:
基本类型数据是否能拷贝?
键和值都是基本类型的普通对象是否能拷贝?
Symbol作为对象的key是否能拷贝?
Date和RegExp对象类型是否能拷贝?
Map和Set对象类型是否能拷贝?
Function对象类型是否能拷贝?(函数我们一般不用深拷贝)
对象的原型是否能拷贝?
不可枚举属性是否能拷贝?
循环引用是否能拷贝?
const Utils={
isFunction: variable => typeof variable === 'function', // 是否是函数
isObject(target) { // 判断是否为object类型
return typeof target === 'object' && target
},
deepClone(data, map = new WeakMap()) {
// WeakMap作为记录对象Hash表(用于防止循环引用)
// 基础类型直接返回值
if (!this.isObject(data) || !this.isFunction(data)) {
return data
}
// 日期或者正则对象则直接构造一个新的对象返回
if ([Date, RegExp].includes(data.constructor)) {
return new data.constructor(data)
}
// 处理函数对象
if (typeof data === 'function') {
return new Function('return ' + data.toString())()
}
// 如果该对象已存在,则直接返回该对象
const exist = map.get(data)
if (exist) {
return exist
}
// 处理Map对象
if (data instanceof Map) {
const result = new Map()
map.set(data, result)
data.forEach((val, key) => {
// 注意:map中的值为object的话也得深拷贝
if (this.isObject(val)) {
result.set(key, this.deepClone(val, map))
} else {
result.set(key, val)
}
})
return result
}
// 处理Set对象
if (data instanceof Set) {
const result = new Set()
map.set(data, result)
data.forEach(val => {
// 注意:set中的值为object的话也得深拷贝
if (this.isObject(val)) {
result.add(this.deepClone(val, map))
} else {
result.add(val)
}
})
return result
}
// 收集键名(考虑了以Symbol作为key以及不可枚举的属性)
const keys = Reflect.ownKeys(data)
// 利用 Object 的 getOwnPropertyDescriptors 方法可以获得对象的所有属性以及对应的属性描述
const allDesc = Object.getOwnPropertyDescriptors(data)
// 结合 Object 的 create 方法创建一个新对象,并继承传入原对象的原型链, 这里得到的result是对data的浅拷贝
const result = Object.create(Object.getPrototypeOf(data), allDesc)
// 新对象加入到map中,进行记录
map.set(data, result)
// Object.create()是浅拷贝,所以要判断并递归执行深拷贝
keys.forEach(key => {
const val = data[key]
if (this.isObject(val)) {
// 属性值为 对象类型 或 函数对象 的话也需要进行深拷贝
result[key] = this.deepClone(val, map)
} else {
result[key] = val
}
})
return result
}
}
10. 实现一个方法给树扩展下列属性(高级)。
// 后代的最底级的孩子个数 maxLeafCount
// 基于其兄长以及兄长后代中自己的编号 startLeafCount
// 其最亲的兄弟的索引 nearestSublingsIndex
// 其族内兄弟的索引 allSublingsIndex,
// 属于第几代 levelNo
// 是否有后代 isLeaf
// 父节点 parentNode
function extensionTreeData(arr, pStartleafCount, levelno = 1, parent, AllSublingsIndexMap = {}) {
return arr.reduce((preResult, next, index) => {
next.parentNode = parent || null
next.levelno = levelno
AllSublingsIndexMap[levelno] = AllSublingsIndexMap[levelno] ? AllSublingsIndexMap[levelno] + 1 : 1
next.nearestSublingsIndex = index + 1
next.allSublingsIndex = AllSublingsIndexMap[levelno]
if (Array.isArray(next.children) && next.children.length) {
if (pStartleafCount === undefined) {
next.startLeafCount = index === 0 ? 1 : preResult + 1
} else {
next.startLeafCount = index === 0 ? pStartleafCount : pStartleafCount + preResult
}
next.isleaf = false
next.maxLeafCount = extensionTreeData(next.children, next.startLeafCount, levelno + 1, next, AllSublingsIndexMap)
return preResult + next.maxLeafCount
} else {
next.isleaf = true
next.maxLeafCount = 1
if (pStartleafCount === undefined) {
next.startLeafCount = index === 0 ? index + 1 : preResult + 1
} else {
next.startLeafCount = index === 0 ? pStartleafCount : pStartleafCount + preResult
}
return preResult + 1
}
}, 0)
// return AllSublingsIndexMap
}
11. 实现一个洋葱模型(高级)
洋葱模型是以next()函数为分割点,先由外到内执行Request的逻辑,然后再由内到外执行Response的逻辑,这里的request的逻辑,我们可以理解为是next之前的内容,response的逻辑是next函数之后的内容,也可以说每一个中间件都有两次处理时机。洋葱模型的核心原理主要是借助compose方法。
调用:
const sleep=time=>new Promise((resolve,reject)=>{setTimeout(()=>{resolve()},time)})
let onionModelCall = new OnionModel()
onionModelCall.use(async (ctx, next) => {
console.log(ctx)
console.log(`第一层 start`)
await sleep(1000)
await next()
await sleep(1000)
console.log(`第一层 end`)
})
onionModelCall.use(async (ctx, next) => {
console.log(`第二层 start`)
await sleep(2000)
await next()
await sleep(2000)
console.log(`第二层 end`)
})
onionModelCall.use(async (ctx, next) => {
console.log(`第三层 start`)
await sleep(3000)
await next()
await sleep(3000)
console.log(`第三层 end`)
})
//执行监听回调,模拟接受请求后的处理
onionModelCall.start()
class OnionModel { // 洋葱模型
middleware = []
constructor(obj) {
this.ctx = obj || {}
}
use(fn) {
this.middleware.push(fn)
}
start() {
let fn = this.compose(this.middleware)
return fn(this.ctx).then(this.respond)
}
respond() {
console.log(`响应数据`)
}
dispatch(index, ctx) {
let fn = this.middleware[index];
if (!fn) return Promise.resolve();
return fn(ctx, () => this.dispatch(index + 1, ctx))
}
compose(middleware) {
let self = this
return function (ctx) {
return self.dispatch(0, ctx);
}
}
}
12. 解析html字符串。写一个方法,将html字符串解析为JSON对象(高级)。
- 考虑三种种节点:element、text,comment
- 需要虑节点的Attributes
- 需要考虑自闭合标签
let sourceStr = '<html lang="en"><head></head><body>hello</body></html>'
解析为JSON对象:
解析后的结果
let resultData = [
{
"name": "html",
"type": "element",
"attributes": {
"lang": "en"
},
"children": [
{
"name": "head",
"type": "element",
"children": []
},
{
"name": "body",
"type": "element",
"children": [
{
"type": "text",
"text": "hello"
}
]
}
]
}
]
class HTMLParser {
constructor(cb) {
this.init()
}
init() {
this.nodeTagRegs = {
startTag: /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/,
endTag: /^<\/([-A-Za-z0-9_]+)[^>]*>/,
attr: /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g,
commentTag: /^<!--(.*?)-->/,
comment: /<!--(.|[\r\n])*?-->/,
tags: /^<(?<tag>[^\s>]+)[^>]*>/
}
this.defaultHandlers = {
parseStartTagCb(node, tag, attrs, isSelfCloseTag, source) { },
parseEndTagCb(tag) { },
parseTextCb(text) { },
parseCommentCb(text) { },
}
}
HTMLParser(html, handler) { // 匹配开始和结束以及其他节点,然后调用handler
let { startTag, endTag, attr, commentTag, tags } = this.nodeTagRegs
let ct, ce, cs, cc
while (html) {
html = html.replace(/^(\s+)?(\n+)?(\r+)?/, '')
if (ct = html.match(commentTag)) {
handler.comments(ct[1])
html = html.replace(commentTag, '')
} else if (ce = html.match(endTag)) {
handler.endTag(ce[1])
html = html.replace(endTag, '')
} else if (cs = html.match(startTag)) {
handler.startTag(cs[1], cs[2], cs[3], cs[0])
html = html.replace(startTag, '')
} else {
let chars = ''
let i = 0
while (!html.match(tags)) {
chars += html[0]
html = html.substring(1)
}
handler.chars(chars)
}
}
}
createNode(node, { tag, attrs, isSelfCloseTag, source }, results) {
try {
let { imagesNodes, imageUrls, hrefNodes, hrefUrl, srcNodes, srcUrls } = results
let { attr } = this.nodeTagRegs;
(attrs.match(attr) || []).forEach(item => {
let attrArr = item.split('=')
node.attributes[attrArr[0]] = (!/"/g.test(attrArr[1]) && /^[0-9.]{1,}$/g.test(attrArr[1])) ? Number(attrArr[1]) : attrArr[1].replace(/"/g, '')
})
node.classList = (node.attributes.class && node.attributes.class.split(' ').filter(item => item)) || [];
((node.attributes.style || '').replace(/(")/ig, "\'")).replace(/\s+/g, '').split(';').forEach((item => {
if (!item) return
let attrArr = item.split(':')
node.style[attrArr[0]] = (!/"/g.test(attrArr[1]) && /^[0-9.]{1,}$/g.test(attrArr[1])) ? Number(attrArr[1]) : attrArr[1].replace(/"/g, '')
}))
if (node.tagName === "IMG") {
var imgUrl = node.attributes.src;
imagesNodes.push(node);
imageUrls.push(imgUrl);
} else if (node.attributes.href) {
node.src = node.attributes.href;
hrefNodes.push(node)
hrefUrls.push(node.attributes.href)
} else if (node.attributes.src) {
node.src = node.attributes.src
srcNodes.push(node)
srcUrls.push(node.attributes.src);
} else { }
return node
} catch (e) {
return node
}
}
HTMLtoJSON(html, handlers) {
let curHandlers = Object.assign({}, this.defaultHandlers, handlers || {})
let { parseStartTagCb, parseEndTagCb, parseTextCb, parseCommentCb } = curHandlers
let nodes = []
let results = { // 结果
imagesNodes: [],
imageUrls: [],
hrefNodes: [],
hrefUrls: [],
srcNodes: [],
srcUrls: [],
text: [],
script: [],
nodes
}
let tagStack = [] // 记录开始和结束,开始入栈push,结束出栈pop
let self = this
this.HTMLParser(html, {
startTag(tag, attrs, isSelfCloseTag, source) {
let node = {
source: source,
node: 'element',
tagName: tag.toUpperCase(),
attributes: {},
style: {},
classList: [],
attrs: attrs.replace(/"/g, ''),
}
let parent = tagStack[tagStack.length - 1]
self.createNode(node, { tag, attrs, isSelfCloseTag, source }, results)
if (!isSelfCloseTag) {
tagStack.push(node)
}
if (parent) {
(parent.children || (parent.children = [])).push(node)
} else {
nodes.push(node)
}
parseStartTagCb(node, tag, attrs, isSelfCloseTag, source)
},
endTag(tag) {
parseEndTagCb(tagStack.pop(), tag)
},
chars(text) {
let node = {
node: 'text',
text: text,
};
let parent = tagStack[tagStack.length - 1] || nodes
if (parent.tagName === 'SCRIPT') {
results.script.push(text)
} else {
results.text.push(text)
}
if (parent) {
(parent.children || (parent.children = [])).push(node)
} else {
nodes.push(node)
}
parseTextCb(node, text)
},
comments(text) {
let node = {
node: 'comment',
text: text,
};
let parent = tagStack[tagStack.length - 1] || nodes
if (parent) {
(parent.children || (parent.children = [])).push(node)
} else {
nodes.push(node)
}
parseCommentCb(text);
},
});
return results;
}
// new HTMLParser().HTMLtoJSON('<html lang="en"><head></head> <!--s1111jjwwghwhwh--> <body class="class" count=12 style="height:10px;width:30px;font-weight:400;"><text>hello world</text><img /></body></html>')
}