收集、整理自己在 zhangxinxu/quiz 上的答题
下述目录按时间倒序排列
-
JS基础测试33:自定义 toFixed 方法 (未录播)
前端小测试答题收集
收集、整理自己在 zhangxinxu/quiz 上的答题
下述目录按时间倒序排列
JS基础测试33:自定义 toFixed 方法 (未录播)
解析:数组中的空元素 (empty 元素) 也会被算到数组长度中
console.log(arr.length); // 4
解析:数组中的 empty 元素不会参与数组项遍历,故只需返回 true 即可过滤掉 empty 元素(而不会牵连 0
、NaN
、null
、undefined
、''
这些)
arr = arr.filter(it => true);
console.log(arr); // [1, 2, 3]
解析:parseInt 接收 string
和 radix
两个参数,前者是待转换的字符串,后者是进制参考基数,默认是 10。当 parseInt 作为 map 方法的回调函数时,parseInt 的 string
的实参是数组项,radix
的实参是数组索引值,故执行过程如下:
parseInt(1, 0); // 1. `radix` 为 0 时会自动转换成 10,故输出 1
parseInt(2, 1); // 2. `radix` 值非法(没有一进制),无法解析,故输出 NaN
parseInt(3, 2); // 3. `string` 为 3 ,`radix` 为 2,无法将 3 按二进制解析,故输出 NaN
let arr2 = arr.map(parseInt);
console.log(arr2); // [1, NaN, NaN]
var arr3 = arr.concat(arr2);
console.log(arr3); // [1, 2, 3, 1, NaN, NaN]
解析:利用 ES6 中的 Set 集合不存在重复项的特点来去重
arr3 = [...new Set(arr3)];
console.log(arr3); // [1, 2, 3, NaN]
没有什么字符串处理是用一个正则表达式解决不了的,如果有,那就用两个,事实上我用了四个。
DEMO 更精彩 (已封装,并提供测试用例)
这是送分题吗?
content.length > 140;
使用正则将所有连续空格、换行替换为1个
content.trim() // 移除两端空格
.replace(/[\r\n]+/g, '\n') // 连续换行合并为 1 个
.replace(/[ ]+/g, ' ') // 内容中的连续空格合并成 1 个
> 140;
原理:将内容按 2 个 ASCⅡ 字符作为切割点划分数组,累加统计数组项里面的字符数,再加上数组长度 - 1 的值,得到最终字数
var arr = content.trim()
.replace(/[\r\n]+/g, '\n')
.replace(/[ ]+/g, ' ')
.split(/[\x00-\xff]{2}?/g); // ASCⅡ 码范围(惰性匹配)
var len = arr.length - 1 + arr.reduce(function(total, cur) {
return total += cur.length
}, 0);
len > 140;
先按题 2 处理,然后使用正则匹配网址并替换为 10 个占位字符,最后按题 3 处理,统计最后字数
function fixASC2CharsNum(str) {
var arr = str.split(/[\x00-\xff]{2}?/g);
return arr.length - 1 +
arr.reduce(function(total, cur) {
return total += cur.length;
}, 0);
}
content = content.trim()
.replace(/[\r\n]+/g, '\n')
.replace(/[ ]+/g, ' ');
var regex_url = /https?:\/\/(:?[\w-]+\.)+[\w-]+\S+\b/g; // 网址(宽松判断)
var shortUrlCharsNum = 10;
var placeholder = Array(shortUrlCharsNum).fill('囧');
content = content.replace(regex_url, function(matched) {
// 小于 10 个字符的不使用短网址
if (fixASC2CharsNum(matched) < shortUrlCharsNum) {
return matched;
}
return placeholder;
});
fixASC2CharsNum(content) > 140;
<div class="btn-group">
<button class="btn btn-danger">按钮</button>
</div>
<div class="btn-group">
<button class="btn btn-danger">按钮</button>
<button class="btn btn-danger">按钮</button>
</div>
<div class="btn-group">
<button class="btn btn-danger">按钮</button>
<button class="btn btn-danger">按钮</button>
<button class="btn btn-danger">按钮</button>
</div>
/* 解法1:grid 方案 */
.btn-group {
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
}
.btn-group + .btn-group {margin-top: 15px;}
.btn {
padding: 6px 0;
text-align: center;
color: #666;
background: transparent;
border: 1px solid;
border-radius: 10em;
}
.btn-danger {color: #ef7077;}
/* 解法2:flex 方案 */
.btn-group {
display: flex;
margin-left: -.5rem;
margin-right: -.5rem;
}
.btn-group + .btn-group {margin-top: 15px;}
.btn {
flex: 1;
padding: 6px 0;
margin: 0 .5rem;
text-align: center;
color: #666;
background: transparent;
border: 1px solid;
border-radius: 10em;
}
.btn-danger {color: #ef7077;}
function trimBlank(strTel) {
return strTel.trim()
}
function doubleByteToSingle(strTel) {
const doubleByteNums = '0123456789';
return strTel.replace(
new RegExp('[' + doubleByteNums + ']', 'g'), matched =>
doubleByteNums.indexOf(matched)
)
}
function rmCountryCode(strTel) {
return strTel.replace(/^\+8\s*6/, '')
}
function rmConnector(strTel) {
return strTel.replace(/\D/g, '')
}
function isValidTel(strTel) {
return /^1\d{10}$/.test(strTel);
}
const tests = [
{ it: ' 13208033621 ', expect: '13208033621', handle: trimBlank },
{ it: '13208033621', expect: '13208033621', handle: doubleByteToSingle },
{ it: '+8613208033621', expect: '13208033621', handle: rmCountryCode },
{ it: '1320-8033-621', expect: '13208033621', handle: rmConnector },
{ it: '1320 8033 621', expect: '13208033621', handle: rmConnector },
{ it: ' +8 613 208-033621 ', expect: true, handle: isValidTel }
];
function trimBlank(strTel) {
return strTel.trim()
}
function doubleByteToSingle(strTel) {
const doubleByteNums = '0123456789';
return strTel.replace(
new RegExp('[' + doubleByteNums + ']', 'g'), matched =>
doubleByteNums.indexOf(matched)
)
}
function rmCountryCode(strTel) {
return strTel.replace(/^\+8\s*6/, '')
}
function rmConnector(strTel) {
return strTel.replace(/\D/g, '')
}
function isValidTel(strTel) {
return /^1\d{10}$/.test(
rmConnector(
rmCountryCode(
doubleByteToSingle(
trimBlank(strTel)
)
)
)
);
}
tests.map(test => {
const result = test.handle(test.it);
const isPassed = result === test.expect;
console.group(`${test.it} -> ${test.expect}`);
isPassed ?
console.log('%c√ Pass', 'color: green') :
console.log('%c× Failed! the actual result is: ' + result, 'color: red');
console.groupEnd();
});
直接使用强大的 Grid 布局,妥妥的。> 在线 DEMO <
<ul class="square-box">
<li class="square-item bg-dark"></li>
<li class="square-item bg-gray"></li>
<li class="square-item bg-sienna active"></li>
<li class="square-item bg-gold"></li>
<li class="square-item bg-crimson"></li>
<li class="square-item bg-orchid"></li>
<li class="square-item bg-silver"></li>
</ul>
/* css reset */
html {font-size: 16px;}
ul {
padding: 0;
margin: 0;
list-style: none;
}
/* 布局 */
.square-box {
display: grid;
grid-template-columns: repeat(5, 1fr); /* 划分 5 个等宽片段 */
grid-template-rows: 1fr 1fr;
grid-gap: .5rem .375rem;
}
.square-item {
padding-bottom: 100%; /* 容器的 padding 百分比值按其宽度来计算 */
outline: .15rem solid transparent;
}
.square-item.active {outline-color: #222;}
/* 颜色 */
.bg-dark {background: #424242;}
.bg-gray {background: #8c8c8c;}
.bg-sienna {background: #d48b69;}
.bg-gold {background: #fecb66;}
.bg-crimson {background: #c54941;}
.bg-orchid {background: #591c93;}
.bg-silver {background: #e6e6e6;}
/* 响应式断点(CSS基础测试4),基于 iPhone6 尺寸 */
@media screen and (min-width: 375px) {
html {
font-size: calc(100% + 2 * (100vw - 375px) / 39);
font-size: calc(16px + 2 * (100vw - 375px) / 39);
}
}
@media screen and (min-width: 414px) {
html {
font-size: calc(112.5% + 4 * (100vw - 414px) / 586);
font-size: calc(18px + 4 * (100vw - 414px) / 586);
}
}
// 方法1:利用 RegExp 的零宽断言
str = str.replace(/fill="(?!none)[^"]+"/gi, '');
// 方法2:先用 RegExp 通用判断,然后 replace 函数进一步判断
str = str.replace(/fill="([^"]+)"/gi, function($0, $1) {
return $1.toLowerCase() === 'none' ? $0 : '';
});
接第 1 题
window.btoa(str)
接第 1 题
var reg_encodeChars = new RegExp('["%#{}<>]', 'g');
str = str.replace(reg_encodeChars, encodeURIComponent);
首先想到了用 display: table
方案,但几经尝试没 hold 住,后改用 float 实现:> 在线 Demo <
- 为了兼容 IE8 下右浮动元素与非浮动元素的对齐,HTML 标签顺序未能与可视效果相一致(界面显示上标题在前,标签在后,而 HTML 代码中标签在前,标题在后);
- 第 2 题,宽度不足时会截断标题文本,其他浏览器是从左侧开始截断的(符合题意),而 IE(8~11)下是从右侧开始截断的。
<!-- 1 -->
<ul class="book-list">
<li class="book-list-item">
<div class="book-tags">
<span class="tag">都市</span>
<span class="tag" theme="red">连载中</span>
<span class="tag" theme="blue">54.82万字</span>
</div>
<h3 class="book-title">
<a class="book-title-link" href="##">这次是一个新的故事。浩劫余生,终见光明</a>
</h3>
</li>
<li class="book-list-item">
<div class="book-tags">
<span class="tag">都市</span>
<span class="tag" theme="red">完本</span>
<span class="tag" theme="blue">1万字</span>
</div>
<h3 class="book-title">
<a class="book-title-link" href="##">穿越天地复苏的平行世界,偶获诸天聊天群</a>
</h3>
</li>
<li class="book-list-item">
<div class="book-tags">
<span class="tag">科幻</span>
<span class="tag" theme="red">完本</span>
<span class="tag" theme="blue">1059.98万字</span>
</div>
<h3 class="book-title">
<a class="book-title-link" href="##">修真四万年</a>
</h3>
</li>
<li class="book-list-item">
<div class="book-tags">
<span class="tag">童话</span>
<span class="tag" theme="red">完本</span>
<span class="tag" theme="blue">1万字</span>
</div>
<h3 class="book-title">
<a class="book-title-link" href="##">The quick brown fox jumps over a lazy dog.</a>
</h3>
</li>
</ul>
<!-- 2 -->
<ul class="book-list is-inverse">
<li class="book-list-item">
<div class="book-tags">
<span class="tag">都市</span>
<span class="tag" theme="red">连载中</span>
<span class="tag" theme="blue">54.82万字</span>
</div>
<h3 class="book-title">
<a class="book-title-link" href="##">这次是一个新的故事。浩劫余生,终见光明</a>
</h3>
</li>
<li class="book-list-item">
<div class="book-tags">
<span class="tag">都市</span>
<span class="tag" theme="red">完本</span>
<span class="tag" theme="blue">1万字</span>
</div>
<h3 class="book-title">
<a class="book-title-link" href="##">穿越天地复苏的平行世界,偶获诸天聊天群</a>
</h3>
</li>
<li class="book-list-item">
<div class="book-tags">
<span class="tag">科幻</span>
<span class="tag" theme="red">完本</span>
<span class="tag" theme="blue">1059.98万字</span>
</div>
<h3 class="book-title">
<a class="book-title-link" href="##">修真四万年</a>
</h3>
</li>
<li class="book-list-item">
<div class="book-tags">
<span class="tag">童话</span>
<span class="tag" theme="red">完本</span>
<span class="tag" theme="blue">1万字</span>
</div>
<h3 class="book-title">
<a class="book-title-link" href="##">The quick brown fox jumps over a lazy dog.</a>
</h3>
</li>
</ul>
.book-list {
padding: 0;
margin: 0;
list-style: none;
}
.book-list-item {
margin: .8em 0;
line-height: 1.2;
font-size: 18px;
}
.book-list-item:after {
content: "";
clear: both;
display: table;
}
.book-title {
margin: 0;
font-size: 100%;
font-weight: 500;
}
.book-title-link {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
text-decoration: none;
color: #9da2a9;
}
.book-title-link:hover {color: #498bed;}
.is-inverse .book-title-link {direction: rtl;}
.book-tags {
float: right;
margin-left: 1em;
}
.tag {
display: inline-block;
padding: 3px;
white-space: nowrap;
line-height: 1;
font-size: 82%;
color: #9da2a9;
border: 1px solid;
border-radius: 2px;
}
.tag[theme="red"] {color: #ee4e54;}
.tag[theme="blue"] {color: #498bed;}
document.querySelectorAll('[type=radio]:required');
// 处于禁用状态的 fieldset 下的 radio 也包含在内
document.querySelectorAll('[type=radio]:disabled');
document.querySelectorAll('[type=radio]:checked');
const activateBtn = document.getElementById('removeDisabled');
activateBtn.addEventListener('click', () => {
const disabledRadios = document.querySelectorAll('[type=radio]:disabled');
[...disabledRadios].map(radio => {
const fieldset = radio.closest('fieldset'); // 这里未考虑多层 fieldset 嵌套的情况
radio.disabled = false;
fieldset && (fieldset.disabled = false);
});
});
纯 CSS 方案即可
[type=radio]:invalid {
outline: 3px dashed red;
}
//zxx: 拖不动,除了拖到最后
wingmeng: 多谢张老师,昨晚太困了 js 逻辑没写完 😢 ,今早才补完的
.img-list {
display: flex;
flex-wrap: wrap;
width: 260px;
background: #fff;
border: 1px solid #ccc;
transition: all .2s;
}
.img-list.is-dragenter {
background: #ffd;
border-color: #333;
}
.img-list > img {
max-width: 33.3333%;
padding: 2px;
box-sizing: border-box;
cursor: move;
}
.img-preview img {
display: block;
max-width: 100%;
}
.img-preview::backdrop {background: rgba(0, 0, 0, .6);}
<div class="img-list" id="imgList">
<img src="https://cdn.pixabay.com/photo/2014/12/30/13/19/girls-583917_960_720.jpg" alt="海边女孩">
<img src="https://cdn.pixabay.com/photo/2019/10/21/10/33/garden-4565700_960_720.jpg" alt="花园">
<img src="https://cdn.pixabay.com/photo/2019/10/27/18/48/chinatown-4582511_960_720.jpg" alt="都市">
<img src="https://cdn.pixabay.com/photo/2016/12/03/14/20/woman-1879905_960_720.jpg" alt="金发女郎">
<img src="https://cdn.pixabay.com/photo/2019/10/08/18/13/matterhorn-4535693_960_720.jpg" alt="山峰">
<img src="https://cdn.pixabay.com/photo/2017/04/22/10/15/sport-2250970_960_720.jpg" alt="运动">
<img src="https://cdn.pixabay.com/photo/2013/06/10/09/23/morocco-123978_960_720.jpg" alt="沙漠">
</div>
const viewImgList = {
init(selector) {
this.el = document.querySelector(selector);
this.dialog = null;
this.bindClick();
this.bindDrag();
},
bindClick() {
// 图片点击事件
this.el.addEventListener('click', e => {
if (e.target.tagName.toLowerCase() === 'img') {
this.togglePreview(e.target.src);
}
});
// 任意位置点击
window.addEventListener('click', e => {
if (!this.el.contains(e.target)) {
this.togglePreview(false);
}
});
},
bindDrag() {
let imgElm = null;
const isInside = elm => elm === this.el || this.el.contains(elm);
// 拖动开始
this.el.addEventListener('dragstart', e => {
e.target.style.opacity = 0;
imgElm = e.target;
});
// 拖动结束
this.el.addEventListener('dragend', e => e.target.style.opacity = '');
// 放置目标
document.addEventListener('dragover', e => {
if (isInside(e.target)) {
e.preventDefault();
if (e.target === this.el) {
this.el.appendChild(imgElm);
} else {
const compareValue = imgElm.compareDocumentPosition(e.target);
if (compareValue === 2) { // 目标图片在前
e.target.before(imgElm); // 添加到目标图片前面
} else if (compareValue === 4) { // 目标对象在后
e.target.after(imgElm); // 添加到目标图片后面
}
}
}
});
// 进入目标区域
document.addEventListener('dragenter', e => {
this.el.classList[isInside(e.target) ? 'add' : 'remove']('is-dragenter');
});
// 允许放置
this.el.addEventListener('drop', e => {
e.preventDefault();
this.el.classList.remove('is-dragenter');
});
},
togglePreview(src) {
if (src) {
this.buildDialog(src);
this.dialog && !this.dialog.open && this.dialog.showModal();
} else {
this.dialog && this.dialog.close();
this.destoryDialog();
}
},
buildDialog(imgSrc) {
const dialog = document.createElement('dialog');
const img = new Image();
dialog.className = 'img-preview';
img.src = imgSrc;
this.dialog = dialog;
dialog.appendChild(img);
document.body.appendChild(dialog);
},
destoryDialog() {
this.dialog && document.body.removeChild(this.dialog);
this.dialog = null;
}
};
viewImgList.init('#imgList');
document.getElementById('loginForm'); // 方法1:经典的 id 选择器
document.querySelector('#loginForm'); // 方法2:万能的 querySelector
document.forms.loginForm; // 方法3:表单原生方法,还可以写作:document.forms['loginForm']
loginForm; // 方法4:标签的 id 可以直接当变量来用
form 标签添加 onsubmit="return false"
<form id="loginForm" action="/account/login" method="post" onsubmit="return false">
form 提交事件里 return false
(仅限 DOM 0 级)
loginForm.onsubmit = function() {
// 其他操作...
return false;
};
form 提交事件里阻止表单默认行为 preventDefault
// DOM 0 级
loginForm.onsubmit = function(e) {
e.preventDefault();
// 其他操作...
};
// DOM 2 级
loginForm.addEventListener('submit', function(e) {
e.preventDefault();
// 其他操作...
}, false);
const btnTypes = ['button', 'submit', 'reset', 'image'];
const formData = [...loginForm]
// 过滤掉表单按钮
.filter(field => !btnTypes.includes(field.type))
// 将表单元素的值封装成 k-v 形式的数据,例如:{username: 'abc'}
.reduce((data, { name, value }) => {
// 这里仅按照题意作简单处理,
// 如果要完善的话,还要考虑单选框、复选框这类 name 值相同的表单元素
data[name] = value;
return data;
}, {});
console.log(formData)
补充:抽空自己封装表单库,意外发现原来根本不用这么折腾,早就有
FormData
这个 JS 原生对象了,它可以将数据编译成键值对,直接供 AJAX 来使用。孤陋寡闻了……
在 AJAX 提交前为 submit
按钮设置 disabled
属性来禁用按钮(同时也禁用了表单提交),等 AJAX 返回后(无论成功失败)再去掉 disabled
属性启用按钮。
为隐藏的输入框加上 form="loginForm"
属性即可。
利用这个特性可以把从属于表单的元素放在任何地方,只需指明该元素的 form 特性值为表单 id 即可,这样该元素就从属于表单了。
<input name="from" type="hidden" form="loginForm">
图片暂缺
function telTrim(str) {
str = String(str);
const s = str.replace(/[\s-]/g, '');
return /^\d{11}$/.test(s) ? s : str;
}
const formElm = document.getElementById('form');
const inputElm = document.getElementById('input');
// 或者
const formElm = document.forms['form'];
const inputElm = formElm['input'];
inputElm.addEventListener('drop', function(e) {
e.preventDefault();
this.value = telTrim(e.dataTransfer.getData('text'));
});
inputElm.addEventListener('paste', function(e) {
e.preventDefault();
const { selectionStart: start, selectionEnd: end } = this; // 文本框光标位置
const text = e.clipboardData.getData('text');
this.setRangeText(text, start, end); // 从当前光标位置追加粘贴的内容
this.value = telTrim(this.value); // 格式化文本框内容
});
formElm.addEventListener('submit', function(e) {
e.preventDefault();
inputElm.value = telTrim(inputElm.value);
// 后续提交表单的操作... 如 ajax 或 this.submit();
});
预期是达到了,就是算法复杂度有点高了…… > 测试 DEMO <
function getRandomSkins(uid, skins) {
skins = skins instanceof Array ? skins : [];
if (!uid) {
return skins;
}
// 1.将 uid 的每个字符串元字符转换为编码值,组合成一个数字
var charCodes = String(uid).split('').reduce(function(total, cur) {
return total + (isNaN(cur) ? cur.charCodeAt() : cur);
}, '');
// 2.使用上一步生成的数字生成一个和皮肤数组长度一致的字符串数字
var seed = String(Math.sin(charCodes)).split('.')[1].substr(0, skins.length);
// 3.将上一步生成的字符串数字打散添加到皮肤数组的每一项中,用于排序
var result = skins.map(function(item, idx) {
item._order = Number(seed[idx]);
return item;
});
return (
// 4.根据 order 排序
result.sort(function(o, p) {
return o._order - p._order;
})
// 5.移除 order,还原数组本来面貌
.map(function(item) {
delete item._order;
return item;
})
)
}
const object1 = {
userid: 123,
username: '王二',
tel: '13208033621'
};
一句话:两者都会对 URL 中的特殊字符进行编码,区别是两者编码的字符范围不一样,前者不会对属于 URI 的特殊字符进行编码,而后者会。
Object.keys(object1)
.map(key => `${key}=${encodeURIComponent(object1[key])}`) // 转码
.join('&');
location.search;
2019-07-25 修改:调整了函数和形参命名,使之更加语义化;增加了测试用例
// zxx: 有bug
function getQueryObj(queryStr = '') {
// 移除前面的 ? 号(如有)
queryStr = queryStr.indexOf('?') === 0 ? queryStr.substr(1) : queryStr;
if (!queryStr) return {};
return queryStr.split('&')
.map(s => s.split('=')) // 拆解 key 和 value
.reduce((obj, [key, value]) => {
value = value ? decodeURIComponent(value) : ''; // 解码
if (key in obj) {
Array.isArray(obj[key]) ? obj[key].push(value) : obj[key] = [obj[key], value];
} else {
obj[key] = value;
}
return obj;
}, {});
}
getQueryObj
测试用例:
getQueryObj(); // 参数为空
// 一般情况
getQueryObj('userid=123&username=%E7%8E%8B%E4%BA%8C&tel=13208033621');
// 空值(username 值为空)
getQueryObj('userid=123&username=&tel=13208033621');
// 多个重复的键名(tel、favorite)
getQueryObj('?userid=123&tel=13208033621&username=%E7%8E%8B%E4%BA%8C&favorite=rap&favorite=Hip%20hop&favorite=basketball&tel=15888888888&tel=18099999999');
.slider {
padding: 5px 0;
position: relative;
margin: 30px 10%;
--percent: 0;
}
.slider-track {
display: block;
width: 100%; height: 6px;
background-color: lightgray;
border: 0; padding: 0;
}
.slider-track::before {
content: '';
display: block;
height: 100%;
background-color: skyblue;
width: calc(1% * var(--percent));
}
.slider-thumb {
position: absolute;
width: 16px; height: 16px;
border: 0; padding: 0;
background: #fff;
box-shadow: 0 0 0 1px skyblue;
border-radius: 50%;
left: calc(1% * var(--percent)); top: 0;
margin: auto -8px;
}
let myslider1 = new Slider({ el: '#box' }); // 指定容器
new Slider({ value: 50 }); // 缺省赋值
new Slider(); // 无参数(插入到 body 标签最后,赋值为 0)
myslider1.val(value); // js 动态赋值
class Slider {
constructor(opts = {}) {
this.el = opts.el;
this.value = opts.value || 0;
this.slider = null;
this.render();
this.bindEvt();
return {
// 赋值方法
val: (value) => {
this.val(value);
}
}
}
// 渲染 DOM
render() {
const container = document.querySelector(this.el);
const slider = document.createElement('div');
this.slider = slider;
// 有缺省值则赋值
if (this.value) {
this.val(this.value);
}
slider.className = 'slider';
slider.innerHTML = (
// 轨道无需获取焦点
`<button class="slider-track" tabindex="-1"></button>
<button class="slider-thumb"></button>`
);
if (container) {
container.appendChild(slider);
} else {
// 若未指定容器,则在 body 标签最后插入 DOM 结构
document.body.appendChild(slider);
}
}
// 绑定事件
bindEvt() {
const { slider } = this;
const slider_track = slider.querySelector('.slider-track');
const slider_thumb = slider.querySelector('.slider-thumb');
let readyMove = false;
const startHandle = e => {
if (e.target === slider_thumb) {
e.stopPropagation();
readyMove = true;
}
};
const moveHandle = e => {
if (readyMove) {
this.computeVal(e);
}
};
const endHandle = () => readyMove = false;
// 点击监听
slider.addEventListener('click', e => {
// 点击轨道
if (e.target === slider_track) {
this.computeVal(e);
}
}, false);
// 键盘监听
slider.addEventListener('keydown', e => {
// 滑块获得焦点
if (document.activeElement === slider_thumb) {
let value = this.val();
switch(e.keyCode) {
case 37: // 左箭头
value--;
break;
case 39: // 右箭头
value++;
break;
}
this.val(value);
}
}, false);
// 开始拖动
slider.addEventListener('touchstart', startHandle);
slider.addEventListener('mousedown', startHandle);
// 拖动中
window.addEventListener('touchmove', moveHandle);
window.addEventListener('mousemove', moveHandle);
// 拖动结束
window.addEventListener('touchend', endHandle);
window.addEventListener('mouseup', endHandle);
}
// 计算当前值
computeVal(e) {
const { width, left } = this.slider.getBoundingClientRect();
let posX = e.pageX;
if (e.touches) { // 兼容移动端
posX = e.touches[0].pageX;
}
this.val((posX - left) / width * 100);
}
// 赋值 & 取值
val(value) {
if (typeof value === 'undefined') {
// 返回当前 slider 的 percent 值
return this.slider.style.getPropertyValue('--percent').trim() || 0;
}
if (isNaN(value)) { // 过滤非法字符
return;
}
// 边界处理
if (value < 0) {
value = 0;
} else if (value > 100) {
value = 100;
}
this.slider.style.setProperty('--percent', value);
}
}
let title = '这是最后一期了,感谢同学们的一路相伴,更要向为47期小测耗费心血的张老师致敬!';
if (title.length > 15) {
title = title.replace(/^(.{6}).*(.{6})$/, '$1...$2');
}
console.log(title); // "这是最后一期...张老师致敬!"
let filename = 'weixin_20191221232646.jpg';
if (filename.length > 15) {
filename = filename.replace(/^(.+)(\.\w+)$/, (_, $1, $2) => {
let tail = '...' + $1.substr(-1) + $2;
return $1.substr(0, tail.length - 1) + tail;
});
}
console.log(filename); // "weixin_...6.jpg"
思路基本理顺了,但实现上仍然有 bug,待进一步优化 😂
function formatTitle(titleStr) {
// emoji 的 unicode 范围实在是太繁杂了,既有双字节的,还有单字节的
// 下面的 emoji 正则表达式参考自 [email protected]
const regexp_emoji = new RegExp('(' +
'(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])' +
'[\ufe0e\ufe0f]?' +
'(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?' +
'(?:\u200d(?:[^\ud800-\udfff]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff])' +
'[\ufe0e\ufe0f]?' +
'(?:[\u0300-\u036f\ufe20-\ufe23\u20d0-\u20f0]|\ud83c[\udffb-\udfff])?)*' +
')', 'g');
const regexp_symbol = /[,;:……——]/;
const emojiArr = titleStr.match(regexp_emoji);
// 将 title 字符串打散成数组,避免双字节 emoji 字符的影响(每个 emoji 算1个字符)
const titleArr = titleStr.split(regexp_emoji).reduce((arr, s) => {
if (emojiArr && emojiArr.includes(s)) {
arr.push(s)
} else {
arr = arr.concat(s.split(''))
}
return arr;
}, []);
if (titleArr.length > 15) {
const half = Math.floor(titleArr.length / 2);
let left = titleArr.slice(0, half);
let right = titleArr.slice(half);
let leftPoint = 0;
let rightPoint = 0;
// 从中心向两端查找第一个 emoji 或中文标点出现的指针位置
left.some((_, idx, arr) => {
const index = arr.length - 1 - idx;
if (emojiArr && emojiArr.includes(arr[index]) || regexp_symbol.test(arr[index])) {
leftPoint = index + 1;
return true;
}
});
right.some((item, idx) => {
if (emojiArr && emojiArr.includes(item) || regexp_symbol.test(item)) {
rightPoint = idx;
return true;
}
});
if (leftPoint === 0 && rightPoint === 0) {
return titleStr.replace(/^(.{6}).*(.{6})$/, '$1...$2');
}
let diffValue;
if (rightPoint > left.length - leftPoint) {
diffValue = 15 - left.length - 3; // 差值
left = left.join('');
right = right.slice(0, rightPoint).join('') + '...' +
right.slice(right.length - 1 - rightPoint).join('');
} else {
diffValue = 15 - right.length - 3; // 差值
left = left.slice(0, left.length - 1 - diffValue).join('') + '...' +
left.slice(leftPoint).join('');
right = right.join('');
}
return left + right;
}
return titleArr.join('');
}
var xhr = new XMLHttpRequest();
// 上传进度(监听 upload 的 onprogress 事件)
xhr.upload.onprogress = function(e) {
var percent = e.loaded / e.total * 100
console.log('上传进度:' + percent + '%');
}
// 请求完成
xhr.onload = function() {
var resOK = 200;
console.log('请求完成');
// 一个完成的请求不一定是成功的请求,故需要判断状态码来确认
if (xhr.status === resOK) {
console.log('请求成功')
}
}
// 请求错误
xhr.onerror = function() {
console.log('请求失败')
}
xhr.open('POST', '/upload', true);
xhr.send(file);
var maxFileSize = 1 * (1024 * 1024); // 1MB
files = [].slice.call(files).filter(function(file) {
return file.size <= maxFileSize
});
这里用 Promise.all
方法无疑是最合适的,但看题目中的 js 代码没有使用 ES6+
,所以这里用 ES5
规范来实现:
// 这里的 files 是第 2 题中处理过的 files
files.map(uploadImg);
function uploadImg(img) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
var resOK = 200;
if (xhr.status === resOK) {
img._uploaded = true
}
}
xhr.onerror = function() {
img._uploaded = false
}
// 请求结束(无论成功与否)
xhr.onloadend = checkUpload;
xhr.open('POST', '/upload', true);
xhr.send(img);
}
function checkUpload() {
var isDone = files.every(function(file) {
return file.hasOwnProperty(_uploaded)
});
if (isDone) {
console.log('上传已结束');
var result = files.reduce(function(count, cur) {
count[cur._uploaded ? 'success' : 'fail'] += 1;
return count;
}, {
success: 0,
fail: 0
});
console.log('上传结果:' +
'成功' + result.success + '个' +
'失败' + result.fail + '个'
);
if (result.success === files.length) {
console.log('图片全部上传成功')
}
}
}
.books {
display: flex;
flex-flow: wrap;
}
.book-item {
margin: 5px;
text-decoration: none;
}
.book-item > .tag {position: absolute;}
.book-item > img {
display: block;
width: 120px;
height: 160px;
}
.tag {
display: inline-block;
padding: 3px 5px;
box-sizing: border-box;
line-height: 1.2;
font-size: 12px;
color: #fff;
background: #cd0000;
}
.tag-top {
width: 28px;
height: 28px;
padding: 2px 4px;
background: linear-gradient(135deg, #cd0000 50%, transparent 0);
}
.tag-hot {
padding-right: 12px;
padding-bottom: 4px;
background: linear-gradient(114deg, #cd0000 calc(100% - 8px), transparent 0);
}
.tag-number {
min-width: 26px;
min-height: 26px;
padding-right: 11px;
border-bottom-right-radius: 100%;
}
<div class="panel login-box">
<div class="panel-head">
<h3 class="panel-title">登录</h3>
</div>
<div class="panel-body">
<form id="loginForm">
<div class="form-group">
<input type="text" class="form-input" placeholder="账号/手机" autofocus required>
</div>
<div class="form-group">
<div class="input-group">
<input type="password" class="form-input" placeholder="密码" required>
<a href="..." class="input-group-addon">忘记密码?</a>
</div>
</div>
<div class="form-group">
<div class="input-group">
<input type="text" class="form-input" placeholder="验证码" autocomplete="off" required>
<a href="..." class="input-group-addon">
<img src="..." alt="验证码" id="validCode" width="86" height="28">
</a>
</div>
</div>
<p id="tipMsg"> </p>
<div class="form-group">
<button type="submit" class="btn btn-primary btn-block">登录</button>
<a href="..." class="btn btn-link btn-block">立即注册</a>
</div>
</form>
</div>
</div>
写之前翻看了下 Chrome 原生 <audio>
的 shadow DOM,有所启发。 > 在线 DEMO <
<div class="audio-controls">
<div class="audio-controls-inner">
<button class="audio-controls-button" aria-label="play"></button>
<span class="audio-controls-time" aria-label="current">0:05</span>
<span class="audio-controls-time" aria-label="remaining">0:05</span>
<input class="audio-controls-timeline" type="range" step="60" max="5000" value="5000">
<button class="audio-controls-button" aria-label="volume">
<span class="audio-controls-button-volume-icon"><i></i></span>
</button>
<button class="audio-controls-button" aria-label="menu"></button>
</div>
</div>
body { background: #d6d6d6; }
/********** 主容器 **********/
.audio-controls {
/* width: 400px; */
text-align: left;
}
/********** 样式容器 **********/
.audio-controls-inner {
font-size: 16px; /* 控件尺寸 */
color: #000; /* 控件主题色 */
background: #f1f3f4; /* 控件背景色 */
display: flex;
align-items: center;
min-width: 2.125em;
height: 3.375em;
padding: 0 .625em;
overflow: hidden;
border-radius: 2.125em;
}
/********** 组件按钮公用样式 **********/
.audio-controls-button {
flex: 0 0 2em;
height: 2em;
padding: 0;
overflow: hidden;
font-size: 100%;
color: inherit;
background: transparent;
border: 0;
border-radius: 2.25em;
cursor: pointer;
transition: background ease-in-out .2s;
}
.audio-controls-button:hover { background: rgba(0,0,0,.06); }
.audio-controls-button::before {
display: inline-block;
content: "";
}
/********** 播放按钮 **********/
.audio-controls-button[aria-label="play"]::before {
transform: translate(30%, .16em);
border: solid transparent;
border-width: .55em .65em;
border-left-color: currentColor;
}
/********** 暂停按钮 **********/
.audio-controls-button[aria-label="pause"]::before {
width: .375em;
height: 48%;
transform: translateY(.16em);
text-align: center;
border: solid currentColor;
border-width: 0 .25em;
}
/********** 声音按钮 **********/
.audio-controls-button[aria-label="volume"] {
margin-left: 1em;
margin-right: .25em;
}
.audio-controls-button[aria-label="volume"]::before { display: none; }
/* 声音按钮图标 */
.audio-controls-button-volume-icon {
display: inline-block;
padding: .3em .15em;
margin-left: -.3em;
transform: translate(-50%, .15em);
border: .3em solid transparent;
border-right-color: currentColor;
box-shadow: inset 1em 0 0;
}
.audio-controls-button-volume-icon > i {
position: relative;
left: calc(.3125em * 2);
}
.audio-controls-button-volume-icon > i::before {
content: "";
position: absolute;
width: .7em;
height: 1.4em;
transform: translateY(-50%);
box-sizing: border-box;
border: .16em solid;
border-left: 0;
border-radius: 0 .7em .7em 0;
}
.audio-controls-button-volume-icon > i::after {
content: "";
position: absolute;
width: .3em;
height: .6em;
transform: translateY(-50%);
background: currentColor;
border-radius: 0 .3em .3em 0;
}
/********** 选项菜单 **********/
.audio-controls-button[aria-label="menu"]::before {
width: .3125em;
height: .3125em;
transform: translateY(-50%);
background: currentColor;
border-radius: 50%;
box-shadow: 0 .5em 0, 0 -.5em 0;
}
/********** 播放时间文本 **********/
.audio-controls-time {
font-size: .875em;
line-height: 1;
opacity: .85;
}
.audio-controls-time[aria-label="current"] {
margin-left: .375em;
}
.audio-controls-time[aria-label="remaining"]::before {
content: "/";
margin: 0 .25em;
}
/********** 时间轴 **********/
.audio-controls-timeline {
-webkit-appearance: none;
flex: 1;
height: .25em;
margin-left: 1em;
font-size: 100%;
color: inherit;
background: currentColor;
border-radius: .25em;
mix-blend-mode: darken;
}
.audio-controls-timeline::-webkit-slider-thumb {
-webkit-appearance: none;
width: 0;
height: 0;
border: .375em solid;
border-radius: 50%;
box-shadow: 50vw 0 0 50vw rgba(255,255,255,.5);
}
移动端长按网页会弹出默认菜单,这个菜单会打断长按滑动操作,被这个问题折磨了好久,后来终于找到根因解决了。避坑指南
let boxNums = 20;
new Box(boxNums); // 调用
class Box {
constructor(nums = 0) {
this.nums = isNaN(nums) ? 0 : Math.floor(nums); // 数量
this.boxes = []; // 当前实例的 .box 元素集合
this.rangeFrame = { // 范围选框
el: null,
x: 0,
y: 0
};
if (nums > 0) {
this.build();
}
}
// 在 body 元素里创建 .box 盒子
build() {
// 使用文档片段,减少性能损耗
let fragment = document.createDocumentFragment();
for (let i = 0; i < this.nums; i++) {
let box = document.createElement('div');
box.className = 'box';
// 阻止移动端长按时弹出系统默认菜单
box.addEventListener('touchstart', e => e.preventDefault());
this.boxes.push(box);
fragment.appendChild(box);
}
document.body.appendChild(fragment);
this.bindEvt();
}
// 绑定操作事件
bindEvt() {
let timer = null;
let isReady = false;
// 按下时
const startHandle = e => {
e.stopPropagation();
let elm = e.target;
// “按压”对象为当前实例中创建的 .box 盒子之一
if (!!~this.boxes.indexOf(e.target)) {
timer = setTimeout(() => {
let { clientX, clientY } = e;
if (e.touches) { // 移动端
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
}
isReady = true;
elm.classList.add('active');
this.buildRangeFrame(clientX, clientY);
}, 350);
} else { // 点击空白处
this.boxes.map(box => box.classList.remove('active'));
}
};
// 拖动中
const moveHandle = e => {
if (isReady) {
let { clientX, clientY } = e;
if (e.touches) { // 兼容移动端
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
}
this.updateRangeFrame(clientX, clientY);
}
};
// 松起时,清空定时器(中断高亮操作)
const endHandle = (e) => {
e.preventDefault();
isReady = false;
clearTimeout(timer);
this.removeRangeFrame();
};
// 按下
window.addEventListener('touchstart', startHandle);
window.addEventListener('mousedown', startHandle);
// 拖动
window.addEventListener('touchmove', moveHandle);
window.addEventListener('mousemove', moveHandle);
// 松起
window.addEventListener('touchend', endHandle);
window.addEventListener('mouseup', endHandle);
}
// 创建范围选框
buildRangeFrame(posX, posY) {
let elm = document.createElement('i');
elm.className = 'range-frame';
elm.style.left = `${posX}px`;
elm.style.top = `${posY}px`;
document.body.appendChild(elm);
this.rangeFrame.el = elm;
this.rangeFrame.x = posX;
this.rangeFrame.y = posY;
}
// 更新范围选框
updateRangeFrame(posX, posY) {
let { el, x, y } = this.rangeFrame;
if (posX < x) { // 向左反方向
el.style.left = 'auto';
el.style.right = `${window.innerWidth - x}px`;
} else {
el.style.left = `${x}px`;
el.style.right = 'auto';
}
if (posY < y) { // 向上反方向
el.style.top = 'auto';
el.style.bottom = `${window.innerHeight - y}px`;
} else {
el.style.top = `${y}px`;
el.style.bottom = 'auto';
}
// 矩形选框尺寸
el.style.width = `${Math.abs(posX - x)}px`;
el.style.height = `${Math.abs(posY - y)}px`;
// 获取矩形区域左上、右下坐标
// this.computeContains({
// x1: Math.min(posX, x), y1: Math.min(posY, y),
// x2: Math.max(posX, x), y2: Math.max(posY, y)
// });
this.computeContains(el.getBoundingClientRect());
}
// 移除范围选框
removeRangeFrame() {
if (this.rangeFrame.el) {
document.body.removeChild(this.rangeFrame.el);
this.rangeFrame.el = null;
}
}
// 计算 box 是否包含在选框区域
computeContains(area) {
this.boxes.map(box => {
let { left, top, width, height } = box.getBoundingClientRect();
// 矩形碰撞检测
if (
area.left + area.width > left && left + width > area.left // 横向
&&
area.top + area.height > top && top + height > area.top // 纵向
) {
box.classList.add('active');
}
});
}
}
// zxx: 框选有bug,一旦框选经过,框选范围再变小的时候,选中态没有还原。
// wingmeng: 谢谢张老师指点,我题意理解错了……
<div class="chatbox">
<article class="message">
<div class="user-avatar">
<img src="https://tva4.sinaimg.cn/crop.0.0.750.750.180/75f2b996jw8f6zkdm7qp7j20ku0kudgr.jpg" alt="提案笙">
</div>
<h4 class="message-heading">
<span>提案笙</span><small>9月30日21:47</small>
</h4>
<div class="message-content">什么秘密,我觉得你现在跟我说什么都没有意义。</div>
</article>
<article class="message">
<div class="user-avatar">
<img src="https://tvax3.sinaimg.cn/crop.135.0.810.810.180/006LO43wly8frjay2sypvj30u00mita5.jpg" alt="淮南王铃">
</div>
<h4 class="message-heading">
<span>淮南王铃</span><small>10月8日10:30</small>
</h4>
<div class="message-content">@蝴蝶蓝 优秀</div>
</article>
<article class="message">
<div class="user-avatar">
<img src="https://tvax4.sinaimg.cn/crop.0.0.512.512.180/6cd7bd09ly8fsnom1e5wtj20e80e8gm7.jpg" alt="蝴蝶蓝">
</div>
<h4 class="message-heading">
<span>蝴蝶蓝</span><small>昨天 22:13</small>
</h4>
<div class="message-content">值得一听~~</div>
</article>
<article class="message oneself">
<div class="user-avatar">
<img src="https://tva1.sinaimg.cn/crop.0.0.750.750.180/006bQeGsjw8f1tgl7z9ncj30ku0kuq44.jpg" alt="Y优秀X">
</div>
<h4 class="message-heading">
<span>Y优秀X</span><small>刚刚</small>
</h4>
<div class="message-content">围观戏精现场</div>
</article>
</div>
html {font-size: 14px;}
body {
padding: 0;
margin: 0;
}
h4 {font-weight: 500;}
.chatbox {
padding-top: 2rem;
background: #fff;
}
.message {
position: relative;
padding: 0 5.5rem;
margin-bottom: 2rem;
font-size: 1.125rem;
}
.user-avatar {
position: absolute;
width: 4rem;
height: 4rem;
left: calc((5.5rem - 4rem) / 2 - 5px);
overflow: hidden;
background: #e6e6e6; /* 背景色兜底,防止头像“走光” */
border-radius: 50%;
}
.user-avatar > img {
display: block;
max-width: 100%;
margin: auto;
}
.message-heading {
margin: 0;
margin-bottom: .4rem;
font-size: inherit;
color: #ababab;
}
.message-heading > span {margin-right: .5em;}
.message-content {
position: relative;
display: inline-block;
min-height: 1em;
padding: .5em 1em;
line-height: 1.6;
background: #f2f4f7;
border-radius: 10px;
border-top-left-radius: 0;
}
.message-content::before {
content: "";
position: absolute;
top: 0; left: 0;
width: calc(5.5rem - 4rem);
height: calc(5.5rem - 4rem);
transform: translateX(-100%);
/* 弧形箭头借鉴了 @guisturdy 的代码 */
box-shadow: 0.5rem 0 0 #f2f4f7;
border-radius: 50% 50% 0 0;
}
.message.oneself {direction: rtl;}
.oneself > .user-avatar {
left: auto;
right: calc((5.5rem - 4rem) / 2 - 5px);
}
.oneself > .message-heading > span {
float: right;
margin-right: 0;
margin-left: .5em;
}
.oneself > .message-content {
direction: ltr;
color: #fff;
background: #00bdfe;
border-radius: 10px;
border-top-right-radius: 0;
}
.oneself > .message-content::before {
left: auto; right: 0;
transform: translateX(1rem);
box-shadow: inset .5rem 0 0 #00bdfe;
}
思路:
:checked
伪类,配合 +(临近选择器) 和 ~(兄弟选择器),来实现子导航展开/折叠的交互效果;max-height
和 opacity
,配合 transition
实现过渡动画效果;max-height
应根据实际情况设置合适的值,太大会造成“卡顿”现象,太小会剪裁掉内容;display: none
或 visibility: hidden
的方式隐藏 checkbox;visibility
来控制元素的可见性,避免元素视觉不可见时(max-height: 0; opacity: 0
),被 Tab 键索引。<main class="container">
<nav class="navbar">
<div class="navbar-group">
<input class="navbar-group-switch" id="switch1" type="checkbox" checked>
<label class="navbar-group-title" for="switch1">布局</label>
<ul class="navbar-group-list">
<li>
<a href="#" class="navbar-link">Flex布局</a>
</li>
<li>
<a href="#" class="navbar-link is-active">Grid布局</a>
</li>
<li>
<a href="#" class="navbar-link">Shapes布局</a>
</li>
<li>
<a href="#" class="navbar-link">Columns布局</a>
</li>
</ul>
</div>
<div class="navbar-group">
<input class="navbar-group-switch" id="switch2" type="checkbox" checked>
<label class="navbar-group-title" for="switch2">组件</label>
<ul class="navbar-group-list">
<li>
<a href="#" class="navbar-link">按钮</a>
</li>
<li>
<a href="#" class="navbar-link">输入框</a>
</li>
<li>
<a href="#" class="navbar-link">下拉列表</a>
</li>
<li>
<a href="#" class="navbar-link">单复选框</a>
</li>
</ul>
</div>
</nav>
</main>
body {
margin: 0;
font-size: 14px;
line-height: 1.42858;
}
.container {
max-width: 1170px;
padding: 0 15px;
margin: 0 auto;
}
/* 导航容器 */
.navbar {
width: 160px;
height: 100vh;
overflow: auto;
color: #353535;
box-shadow: inset 1px 0 0 #e0e0e0,
inset -1px 0 0 #e0e0e0;
}
/* 导航分组标题(点击折叠/展开子导航) */
.navbar-group-title {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
font-weight: 700;
cursor: pointer;
}
.navbar-group-title::after {
content: "";
width: 6px;
height: 6px;
transform: rotate(225deg);
border-style: solid;
border-width: 2px 0 0 2px;
transition: transform .3s;
}
/* 子导航列表 */
.navbar-group-list {
max-height: 0; /* 用以实现过渡动画 */
padding: 0;
margin: 0;
visibility: hidden; /* 元素不可见,避免被 tab-index 索引 */
opacity: 0; /* 用以实现过渡动画 */
overflow: hidden;
transition: all ease-in-out .3s;
list-style: none;
}
/* 导航链接 */
.navbar-link {
display: block;
padding: 0 calc(10px + 1.5em);
line-height: 40px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-decoration: none;
color: inherit;
}
.navbar-link:hover,
.navbar-link.is-active {
color: #36b3ee;
background: rgb(54, 179, 238, .1);
}
/* 控制子导航展开/折叠的复选框 */
.navbar-group-switch {
/* 剪切式隐藏,保留键盘可访问性 */
position: absolute;
clip: rect(0, 0, 0, 0);
}
.navbar-group-switch:focus + .navbar-group-title {
background: rgb(54, 179, 238, .1);
}
.navbar-group-switch:checked + .navbar-group-title::after {
transform: rotate(45deg);
}
.navbar-group-switch:checked ~ .navbar-group-list {
max-height: 500px; /* 根据实际情况合理设置其值 */
visibility: visible;
opacity: 1;
}
var backCode = '6222081812002934027';
// 方法1
var result = backCode.replace(/(\d{4})/g, '$1 ');
console.log(result); // "6222 0818 1200 2934 027"
// 方法2
var result = backCode.split(/(\d{4})/).filter(s => !!s).join(' ');
console.log(result); // "6222 0818 1200 2934 027"
var numberCode = ‘5702375’;
var result = numberCode.replace(/\d{1,3}(?=(\d{3})+$)/g, '$&,'); // 未考虑小数情况
console.log(result); // "5,702,375"
var filesize = 2837475;
function matchUnit(value) {
var sizeUnits = ['K', 'M', 'G'];
var sizeRadix = 1024;
if (value < sizeRadix) {
return (value / sizeRadix).toFixed(1) + sizeUnits[0]
}
for (var i = sizeUnits.length - 1; i >= 0; i--) {
var radix = Math.pow(sizeRadix, i + 1);
if (value >= radix) {
return (value / radix).toFixed(1) + sizeUnits[i]
}
}
}
console.log(matchUnit(filesize)); // 2.7M
console.log(matchUnit(100)); // 0.1K
console.log(matchUnit(10000)); // 9.8K
console.log(matchUnit(100000000)); // 95.4M
console.log(matchUnit(10000000000)); // 9.3G
let a_labels = document.querySelectorAll('a');
let links = document.querySelectorAll(':link');
使用
querySelectorAll
获取的元素可直接用forEach
遍历,或者转换为 Array 类型再遍历
// 3.1
[].slice.call(links).filter(link => {
if (/^javascript:/i.test(link.href)) {
link.setAttribute('role', 'button');
return false;
}
return true;
})
// 3.2
.filter(link => {
// 锚点链接
if (/^#/.test(link.href)) { // zxx: 有bug
return true;
}
// 站外链接
if (!(new RegExp(`^${location.host}`, 'i')).test(link.host)) {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'external nofollow noopener');
return false;
}
})
// 3.3
.forEach(link => {
// 此处接收到的数组只包含以 # 开头的锚点元素(3.2 中已过滤)
link.setAttribute('rel', 'internal');
});
var testSpecs = [
'在LeanCloud上,数据存储是围绕AVObject进行的。',
'今天出去买菜花了 5000元。',
'我家的光纤入户宽带有 10Gbps,SSD 一共有 10TB。显示器分辨率宽度是1920px。',
'今天是 233 ° 的高温。新 MacBook Pro 有 15 % 的 CPU 性能提升。',
'刚刚买了一部 iPhone ,好开心 !',
'她竟然对你说「喵」?!?!??!!喵??!!Meow...',
'你好,我是破折号——一个不苟言笑的符号。',
'核磁共振成像 (NMRI) 是什么原理都不知道? JFGI!',
'这件蛋糕只卖 1000 元。',
'乔布斯那句话是怎么说的?「Stay hungry,stay foolish。」',
'推荐你阅读《Hackers&Painters:Big Ideas from the Computer Age》,非常的有趣。'
];
charCheck(testSpecs.join(''));
function charCheck(str) {
// 枚举类型的符号
var symbols = {
full: '!()【】『』「」《》“”‘’;:,。?、',
half: '!-_()[]{}<>"\';:,./?`',
getRegStr: function(key) {
var symbols = typeof key === 'string' ? this[key] : (function(that) {
if (key instanceof Array) {
return key.reduce(function(total, cur) {
return total += that[cur];
}, '');
}
return '';
})(this);
// 返回符合 regexp 语法的字符串
return symbols.split('').map(function(s) {
return '\\' + s;
}).join('|');
},
getRegRule: function(key, usedAs) {
var strs = this.getRegStr.call(this, 'full');
var regArr = ['(\\S+)', '([' + strs + '])'];
var temp = [].concat(regArr);
temp.reverse();
if (usedAs === 'rule') {
return new RegExp(
'(:?' + regArr.join('\\s+') + ')|' +
'(:?' + temp.join('\\s+') + ')', 'g');
} else if (usedAs === 'format') {
return [
new RegExp(regArr.join('\\s+'), 'g'),
new RegExp(temp.join('\\s+'), 'g')
];
}
}
};
var regExps = [
{
rule: /([\u4e00-\u9fa5]+[a-zA-Z]+)|([a-zA-Z]+[\u4e00-\u9fa5]+)/g,
format: [
/([\u4e00-\u9fa5]+)([a-zA-Z]+)/g,
/([a-zA-Z]+)([\u4e00-\u9fa5]+)/g
],
matches: '$1 $2',
msg: '中英文之间需要增加空格'
}, {
rule: /([\u4e00-\u9fa5]+\d+)|(\d+[\u4e00-\u9fa5]+)/g,
format: [
/([\u4e00-\u9fa5]+)(\d+)/g,
/(\d+)([\u4e00-\u9fa5]+)/g
],
matches: '$1 $2',
msg: '中文与数字之间需要增加空格'
}, {
rule: /(\d)([A-Z]+)/g,
matches: '$1 $2',
msg: '数字与大写英文单位之间需要增加空格'
}, {
rule: /(\d+)\s+(°|%)/g,
matches: '$1$2',
msg: '° 或 % 与数字之间不需要空格'
}, {
rule: symbols.getRegRule('full', 'rule'),
format: symbols.getRegRule('full', 'format'),
matches: '$1$2',
msg: '全角标点与其他字符之间不加空格'
}, {
// rule: new RegExp('(' + symbols.getRegStr(['full', 'half']) + ')\\1+', 'g'),
rule: new RegExp('(' + symbols.getRegStr('full') + ')\\1+', 'g'),
matches: '$1',
msg: '不重复使用中文标点符号'
}, {
rule: /(\S)(——)(\S)/g,
matches: '$1 $2 $3',
msg: '破折号前后需要增加一个空格'
}, {
// 这条必须位于“遇到完整的英文整句、特殊名词,其內容使用半角标点”之前
rule: new RegExp('\\s*(' + symbols.getRegStr('half') + ')\\s*', 'g'),
matches: function(s) {
s = s.replace(/(^\s*)|(\s*$)/g, '');
return String.fromCharCode(s.charCodeAt() + 65248);
},
msg: '使用全角中文标点'
}, {
rule: /[\uFF10-\uFF19]/g,
matches: function(s) {
// 半角字符与全角字符的 charCode 相差 65248
return String.fromCharCode(s.charCodeAt() - 65248);
},
msg: '数字使用半角字符'
}, {
// 中文的句号“。”不是全角字符,需要特殊处理
rule: /[《|「](:?\s*[a-zA-Z]+\s*(。|[\uff00-\uffff])*\s*[a-zA-Z]*)+[\.|」|》]/g,
matches: function(s) {
return s.replace(/。|[\uff00-\uffff]/g, function($1) {
var half = String.fromCharCode($1.charCodeAt() - 65248);
if (!!~['&', '-', '+'].indexOf(half)) { // 需要前后加空格的字符
half = ' ' + half + ' ';
} else if (!!~[':', ',', ';'].indexOf(half)) { // 需要后面加空格的字符
half += ' ';
} else if (half === 'ㄢ') {
half = '.';
}
return half;
});
},
msg: '遇到完整的英文整句、特殊名词,其內容使用半角标点'
}
];
var result = str;
regExps.forEach(function(reg, idx) {
var format = reg.format;
var matches = reg.matches;
var tip = str.match(reg.rule);
if (tip) {
console.group('%c' + reg.msg + ' (X)', 'color: red');
console.log(tip.join('\n'));
console.groupEnd();
if (!format) {
result = result.replace(reg.rule, matches);
} else if (format instanceof Array) {
format.forEach(function(fmtReg) {
result = result.replace(fmtReg, matches);
});
} else if (Object.prototype.toString.call(format) === '[object RegExp]') {
result = result.replace(format, matches);
}
}
});
if (result === str) {
console.log('%c当前文本符合规范 (√)', 'color: green');
return;
}
console.group('按规范格式化后的文本:');
console.log('%c' + result, 'color: green');
console.groupEnd();
}
<div class="btn-group-container">
<div class="btn-group">
<details class="details-menu">
<summary class="details-menu-btn btn btn-default" role="button" aria-label="watch">
<svg class="svg-icon">
<use xlink:href="#icon-watch"></use>
</svg>
Unwatch
</summary>
<div class="details-menu-modal">
菜单(略)
</div>
</details>
<a class="btn btn-link" href="#" aria-label="63 users are watching this repository">63</a>
</div>
<div class="btn-group">
<button class="btn btn-default" aria-label="star">
<svg class="svg-icon">
<use xlink:href="#icon-star"></use>
</svg>
Star
</button>
<a class="btn btn-link" href="#" aria-label="445 users starred this repository">445</a>
</div>
<div class="btn-group">
<button class="btn btn-default" aria-label="fork">
<svg class="svg-icon">
<use xlink:href="#icon-fork"></use>
</svg>
Fork
</button>
<a class="btn btn-link" href="#" aria-label="25 users forked this repository">25</a>
</div>
</div>
<svg style="display:none">
<!-- svg sprite (代码略) -->
</svg>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
}
/** 主容器 **/
.btn-group-container {display: flex;}
/** 按钮组 **/
.btn-group {
display: flex;
align-items: center;
}
.btn-group + .btn-group {margin-left: 10px;}
.btn-group .btn {border-radius: 0;}
.btn-group > .btn:not(:first-child),
.btn-group > .details-menu:not(:first-child) {margin-left: -1px;}
.btn-group > .btn:first-child,
.btn-group > .details-menu:first-child > .btn {
border-top-left-radius: .25em;
border-bottom-left-radius: .25em;
}
.btn-group > .btn:last-child,
.btn-group > .details-menu:last-child > .btn {
border-top-right-radius: .25em;
border-bottom-right-radius: .25em;
}
/** 按钮 **/
.btn {
position: relative;
display: inline-flex;
align-items: center;
padding: 3px 9px;
font-size: 12px;
font-weight: 600;
line-height: 20px;
white-space: nowrap;
vertical-align: middle;
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
font-family: inherit;
background-repeat: repeat-x;
background-position: -1px -1px;
background-size: 110% 110%;
border: 1px solid #ccced0;
border-radius: .25em;
text-decoration: none;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
}
.btn:hover {z-index: 1;}
.btn > .svg-icon {
width: 20px;
height: 20px;
margin-right: 2px;
fill: currentColor;
speak: none;
}
/** 按钮主题定义 **/
.btn-default {
color: #24292e;
background-color: #eff3f6;
background-image: linear-gradient(-180deg, #fafbfc, #eff3f6 90%);
}
.btn-default:hover {
background-color: #e6ebf1;
background-image: linear-gradient(-180deg, #f0f3f6, #e6ebf1 90%);
background-position: -.5em;
border-color: #a5a8ac;
}
.btn-link {
color: #24292e;
background-color: #fff;
}
.btn-link:hover {color: #0366d6;}
/** 按钮下拉菜单 **/
.details-menu {
position: relative;
display: block;
}
.details-menu-btn::-webkit-details-marker {display:none;}
.details-menu-btn::after {
content: "";
display: inline-block;
width: 0;
height: 0;
margin-left: 3px;
transform: translateY(25%);
border: 4px solid transparent;
border-top-color: currentColor;
}
.details-menu-modal {
position: absolute;
}
//zxx: 1,3测试没过
function toCamelCase(str) {
return str.replace(/-([a-z])/g, ($1, $2) => $2.toUpperCase());
}
function toDashJoin(str) {
return str.replace(/[A-Z]/g, '-$&').toLowerCase(); // $&:表示匹配到的内容
}
function toCapitalize(str) {
return str.split(/\s+/).map(s => s.substr(0, 1).toUpperCase() + s.substr(1)).join(' ');
}
function toBetterUrl(str) {
return str.split(/\s+/).map(s => s.toLowerCase()).join('-');
}
补全代码,完成两栏垂直居中布局,地址:http://quiz.xiliz.com/css-quiz16.html
每个方法2积分,每个方法都注意视觉还原效果,如果效果不佳会酌情扣分。
/* 方法1(IE8+) */
.quiz {
display: table;
}
.quiz-h {
padding: 0 10px;
margin: 0;
vertical-align: middle;
}
.quiz-p {
margin: 0 5px;
}
/* 方法2(IE10+) */
.quiz {
align-items: center;
}
.quiz-h {
padding: 0 10px;
margin: auto;
}
.quiz-p {
flex: auto;
margin: 0 5px;
}
/* 方法3(IE8+) */
.quiz {
white-space: nowrap;
}
.quiz-h {
margin: 0;
padding: 0 10px;
vertical-align: middle;
}
.quiz-p {
margin: 0;
margin-right: 90px; /* .quiz-h 的宽度(79.2px)+ 间距(10px) */
vertical-align: middle;
white-space: normal;
}
/* 方法4(IE8+) */
.quiz {
position: relative;
}
.quiz-h {
top: 0;
bottom: 0;
height: 1.5em;
margin: auto 10px;
}
.quiz-p {
margin: 0 5px 0 80px;
}
function colorPad(hex) {
const reg = /^#?([0-9a-f]{1,3}|[0-9a-f]{6})$/i;
let result = '#' + '0'.repeat(6);
if (reg.test(hex)) {
result = hex.replace(reg, (_, $1) => {
$1 = $1.toLowerCase();
switch($1.length) {
case 6:
return '#' + $1;
case 3:
return '#' + $1.split('').map(s => s.repeat(2)).join('');
default:
return '#' + $1.repeat(6 / $1.length);
}
});
}
return result;
}
// http://randomcolour.com/
function randomHex() {
const range = Math.pow(16, 6) - 1;
let color = Math.floor(Math.random() * range).toString(16);
color = '#' + ('0'.repeat(6) + color).slice(-6);
return color;
}
// zxx: 我的测试用例测试下来有问题
function colorRange(a, b) {
let start = parseInt(colorPad(a).substr(1), 16);
let end = parseInt(colorPad(b).substr(1), 16);
if (start > end) {
[start, end] = [end, start];
}
return '#' + (Math.round(Math.random() * (end - start)) + start).toString(16);
}
https://codepen.io/wingmeng/pen/MWWmbLO
// colorPad 测试
const testCases = [
{ it: '0', expect: '#000000' },
{ it: '#0', expect: '#000000' },
{ it: 'a', expect: '#aaaaaa' },
{ it: '#A', expect: '#aaaaaa' },
{ it: 'AF', expect: '#afafaf' },
{ it: '#fc0', expect: '#ffcc00' },
{ it: '#936c', expect: '#000000' },
{ it: '#936ce', expect: '#000000' },
{ it: '#936cef', expect: '#936cef' },
{ it: '#EDFGHI', expect: '#000000' }
];
testCases.map(c => {
const result = colorPad(c.it);
const isPassed = result === c.expect;
console.group(`colorPad("${c.it}") -> "${c.expect}"`);
isPassed ?
console.log('%c√ Pass', 'color: green') :
console.log('%c× Failed! the actual result is: ' + result, 'color: red');
console.groupEnd();
});
// 随机 hex 颜色测试
const testNumbers = 10;
const hexReg = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
[...new Array(testNumbers)].map(() => {
const result = randomHex();
console.group(`randomHex() -> "${result}"`);
hexReg.test(result) ?
console.log('%c√ Pass', 'color: green') :
console.log('%c× Failed', 'color: red');
console.groupEnd();
});
// 颜色范围测试
[...new Array(testNumbers)].map(() => {
const color1 = randomHex();
const color2 = randomHex();
const result = colorRange(color1, color2);
console.group(`colorRange("${color1}", "${color2}") -> "${result}"`);
hexReg.test(result) ?
console.log('%c√ Pass', 'color: green') :
console.log('%c× Failed', 'color: red');
console.groupEnd();
});
第 1 题:document.cookie
第 2 题:document.cookie = 'userid=1';
第 3 题:
var expires = 24 * 36e5; // 过期时间:1 天
document.cookie = 'userid=1; expires=' + (new Date((+new Date(time)) + expires)).toUTCString();
getCookie('_csrfToken');
function getCookie(name) {
var arr = document.cookie.split(/;\s/);
for (var i = 0; i < arr.length; i++) {
var params = arr[i].split('=');
if (name === params[0]) {
return params[1];
}
}
return '';
}
document.cookie = 'ywkey=; expires=' + new Date(Date.now() - 1e10).toUTCString();
第 6 题:localStorage.setItem('userid', 1);
第 7 题:
var expires = 24 * 36e5 * 7; // 过期时间:7 天
// 存储时加个时间戳
localStorage.setItem('userid', JSON.stringify({
data: 1,
stamp: Date.now() + expires
}));
// 取值时进行判断
var store_userid = JSON.parse(localStorage.getItem('userid'));
var uidStamp = store_userid.stamp;
var userid;
if (uidStamp <= Date.now()) { // 过期
localStorage.removeItem('userid');
} else {
userid = store_userid.data;
}
//zxx: Firefox是有打点效果的
.list {
width: 300px;
box-sizing: border-box;
line-height: 1.25;
font-size: 13px;
background: #fff8dc;
border: 1px solid #e6dfc6;
border-radius: 2px;
}
.list-title,
.list-item {padding: 12px 15px;}
.list-title {
font-weight: 700;
color: #737063;
background: #f7f1d5;
}
.list-item {
display: flex;
margin: 0;
border-top: 1px solid #ede7cd;
}
.list-item > a {
/* 仅 Webkit 内核浏览器支持多行溢出打点效果,其他浏览器只截断 */
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
/* 设置最小和最大高度 */
min-height: calc(13px * 1.25 * 1.25);
max-height: calc(13px * 1.25 * 2); /* 字体尺寸×行高×行数(2行) */
overflow: hidden;
text-overflow: ellipsis;
text-decoration: none;
color: #0077cc;
}
.list-item > .iconfont {
margin-right: .5em;
font-size: 16px;
}
.list-item > .icon-dialog {color: #46a2d9;}
.list-item > .icon-stack-overflow {color: #52575c;}
<!-- 使用了第三方 iconfont 资源 -->
<dl class="list">
<dt class="list-title">标题</dt>
<dd class="list-item">
<i class="iconfont icon-dialog"></i>
<a href="##">恭喜我们29个最老的测试版网站-他们现在不再是测试版了!</a>
</dd>
<dd class="list-item">
<i class="iconfont icon-dialog"></i>
<a href="##">《独角兽动物园》35;7:Nicolas访谈</a>
</dd>
<dd class="list-item">
<i class="iconfont icon-stack-overflow"></i>
<a href="##">减少需要重新审查的封闭问题数量的建议</a>
</dd>
<dd class="list-item">
<i class="iconfont icon-stack-overflow"></i>
<a href="##">实验:在接下来的30天内(直到2019-09-07),以3票为基准关闭和重新打开提出的问题</a>
</dd>
</dl>
body {margin: 0;}
main, content, footer {display: block;}
.container {
display: flex;
flex-flow: column;
min-height: 100vh; /* 不设置固定高度 */
}
.content {flex: 1;}
.footer {
color: #fff;
background: #555;
}
const viewList = {
init() {
this.input = document.getElementById('input');
this.list = document.getElementById('list');
this.listItems = this.list.querySelectorAll('li');
this.selectedIdx = -1;
this.clickHandle();
this.keyingHandle();
},
clickHandle() {
this.listItems.forEach((li, index) => {
li.addEventListener('click', () => this.doSelect(index));
});
},
keyingHandle() {
document.addEventListener('keydown', ({ keyCode }) => {
const maxIndex = this.listItems.length - 1;
let index = this.selectedIdx;
if (keyCode === 38) { // ↑
index--
} else if (keyCode === 40) { // ↓
index++
}
index = index < 0 ? maxIndex : index;
index = index > maxIndex ? 0 : index;
this.doSelect(index);
});
},
doSelect(index) {
const curLi = this.listItems[index];
const selectedIdx = this.selectedIdx < 0 ? 0 : this.selectedIdx;
this.listItems[selectedIdx].classList.remove('selected');
curLi.classList.add('selected');
this.input.value = curLi.innerText;
this.selectedIdx = index;
}
};
viewList.init();
html {font-size: calc(16 / 375 * 100vw);}
.grid-nav {
display: grid;
grid-template-columns: repeat(3, 1fr);
overflow: hidden; /* 隐藏溢出的 box-shadow */
background: #524940;
}
.grid-nav-item {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: calc((100% - 1rem * 1.15 - 2rem - .5rem) / 2) 0; /* 正方形容器 */
line-height: 1.15;
color: #f7f1ee;
box-shadow: 16px 0 0 -15px rgba(255, 255, 255, .5), /* 单侧投影 */
0 16px 0 -15px rgba(255, 255, 255, .5);
text-decoration: none;
}
.grid-nav-item > .iconfont {
margin-bottom: .5rem;
font-size: 2rem;
line-height: 1;
}
<nav class="grid-nav">
<a href="#" class="grid-nav-item">
<i class="iconfont icon-clock" aria-hidden="true"></i>
<span>商品秒杀</span>
</a>
<a href="#" class="grid-nav-item">
<i class="iconfont icon-business" aria-hidden="true"></i>
<span>企业团购</span>
</a>
<a href="#" class="grid-nav-item">
<i class="iconfont icon-private" aria-hidden="true"></i>
<span>私码通道</span>
</a>
<a href="#" class="grid-nav-item">
<i class="iconfont icon-sim-card" aria-hidden="true"></i>
<span>铁粉卡</span>
</a>
<a href="#" class="grid-nav-item">
<i class="iconfont icon-exchange" aria-hidden="true"></i>
<span>以旧换新</span>
</a>
<a href="#" class="grid-nav-item">
<i class="iconfont icon-phone-bill" aria-hidden="true"></i>
<span>话费充值</span>
</a>
</nav>
const dialog = document.createElement('dialog');
document.body.appendChild(dialog);
// 方法1:
dialog.open = true; // 或其他为“真值”的基本类型,如 1,"abc" 等
// 方法2:
dialog.show();
// 方法3:
dialog.showModal();
const btn = document.createElement('button');
btn.innerText = 'close';
btn.addEventListener('click', () => {
// 方法1:
dialog.open = false; // 或其他“非真”的基本类型,如 0、null 等
// 方法2:
dialog.close();
});
dialog.appendChild(btn);
// 用 showModal 方法即可让打开的 dialog 自带遮罩,在 CSS 里可通过 ::backdrop 设置遮罩层样式
dialog.showModal();
思路:如果是
showModal()
方法打开的 dialog,则其覆盖层级默认是置顶的;而通过show()
方法或open
属性打开的 dialog,其覆盖层级遵循“后来居上”原则,所以需要手动调整其z-index
值来使其覆盖层级置顶。
(function(dialogElm) {
if (!dialogElm) return;
const proto = dialogElm.prototype;
const oldShow = proto.show;
const dialogBaseLevel = 100; // 对话框弹层的基准层级(根据项目zIndex规范合理设置)
const getMaxZIndex = () => {
const dialogs = document.querySelectorAll('dialog[open]');
const maxZIndex = Math.max.apply(null, [...dialogs].map(it =>
it.style.zIndex || Number(window.getComputedStyle(it).zIndex) || dialogBaseLevel
));
return maxZIndex;
};
const setMaxZIndex = el => el.style.zIndex = getMaxZIndex() + 1;
// 重写 show 方法
proto.show = function() {
setMaxZIndex(this);
oldShow.call(this);
};
// "劫持" open 属性
Object.defineProperty(proto, 'open', {
set: function(value) {
const isOpen = Boolean(isNaN(value) ? value : Number(value));
this[isOpen ? 'show' : 'close']();
}
});
})(window.HTMLDialogElement);
第 1 题和第 2 题,两大问题,一个对策。已封装成组件,只需为构造函数传入需要“返回顶部”功能的容器元素即可,默认为 body 元素。
.back-top {
position: fixed;
margin-top: -15px;
margin-left: -15px;
transform: translate(-100%, -100%);
white-space: nowrap;
}
/**
* 返回顶部
* @descr: 给定一个容器,当滚动高度超出1倍的容器高度,显示返回顶部,否则隐藏
* @param {object HTMLElement} [elm] - HTML元素,可选,默认为 body 元素
*/
function ScrollBackTop(elm) {
var that = this;
var timer = null;
this.elm = elm || document;
this.elmBox = elm || document.documentElement;
this.render();
// 监听容器 scroll 事件
this.elm.addEventListener('scroll', function() {
clearTimeout(timer);
timer = setTimeout(function() {
that.scrolling();
}, 20);
}, false);
// 监听视窗尺寸改变
window.addEventListener('resize', function() {
clearTimeout(timer);
timer = setTimeout(function() {
that.scrolling();
}, 20);
}, false);
}
// 生成按钮
ScrollBackTop.prototype.render = function() {
var that = this;
var btn = document.createElement('a');
var text = document.createTextNode('返回顶部↑');
btn.className = 'back-top';
btn.href = '#';
btn.hidden = true;
// 监听点击
btn.addEventListener('click', function(event) {
if (that.elm === document) {
window.scrollTo(0, 0); // 兼容移动端
} else {
that.elmBox.scrollTop = 0;
}
event.preventDefault();
return false;
}, false);
btn.appendChild(text);
(this.elm === document ? document.body : this.elm).appendChild(btn);
this.btn = btn;
}
ScrollBackTop.prototype.scrolling = function() {
var elmBox = this.elmBox;
var scroll_y = this.elm === document ? window.pageYOffset : elmBox.scrollTop;
var offset_x = elmBox.offsetLeft;
var offset_y = elmBox.offsetTop;
var x = elmBox.clientWidth;
var y = elmBox.clientHeight;
if (scroll_y >= y) {
this.btn.hidden = false;
// 按钮的显示位置
this.btn.style.top = offset_y + y + 'px';
this.btn.style.left = offset_x + x + 'px';
} else {
this.btn.hidden = true;
}
}
new ScrollBackTop(); // 整个文档返回顶部的场景
new ScrollBackTop(document.getElementById('divBox')); // 某个 div 容器的场景
既然题目中用了 let
,那我就不客气了,ES6 直接用起。:tada:
主要原理是数组的 splice
操作,然后是处理各种边界情况。
封装了一下,使用链式调用,使方法看起来更语义化。
let arr = [0, 1, 2, 3, 4, 5];
// 返回对应索引的数组项
Array.prototype.index = function(index) {
if (!(typeof index !== 'boolean' && Number.isInteger(Number(index)))) {
throw (`\`${index}\` is not a integer.`);
}
let arr = [...this]; // 使用原数组的副本,以免后续操作影响原数组
return {
arr,
index,
value: arr[index]
};
}
// 移动数组项
Object.prototype.moveTo = function(target) {
let { arr, index, value } = this;
const _moveTo = (targetIdx) => {
if (!Number.isInteger(targetIdx)) {
throw (`\`${targetIdx}\` is not a integer.`);
}
if (targetIdx > arr.length - 1) {
targetIdx = arr.length - 1;
} else if (targetIdx < 0) {
targetIdx = 0;
}
arr.splice(index, 1); // 先删除原来的
arr.splice(targetIdx, 0, value); // 在目标索引补上原来的
};
if (!arr.includes(value)) { // 不存在
throw (`The current index(\`${index}\`) of item is not in the array([${arr}])`);
}
// 各种条件判断
if (target === 'start' || target <= 0) { // 移到开头
_moveTo(0)
} else if (target === 'end' || target >= arr.length - 1) { // 移到结尾
_moveTo(arr.length);
} else if (target === 'forward') { // 向前
_moveTo(index - 1);
} else if (target === 'backward') { // 向后
_moveTo(index + 1);
} else {
_moveTo(target);
}
return arr;
}
// 交换数组项位置
Object.prototype.swapWith = function(idx) {
let { arr, index, value } = this;
if (!arr.includes(value)) { // 不存在
throw (`The current index(\`${idx}\`) of item is not in the array([${arr}])`);
}
if (!(typeof idx !== 'boolean' && Number.isInteger(Number(idx)))) {
throw (`\`${idx}\` is not a integer.`);
}
if (idx > arr.length - 1 || idx < 0 ) {
throw (`The index(\`${idx}\`) of item is not in the array([${arr}])`);
}
arr[index] = arr.splice(idx, 1, value)[0];
return arr;
}
console.log(arr.index(3))
console.group('任意数组项向前移一位');
console.log(arr.index(0).moveTo('forward')); // [0, 1, 2, 3, 4, 5]
console.log(arr.index(3).moveTo('forward')); // [0, 1, 3, 2, 4, 5]
console.log(arr.index(5).moveTo('forward')); // [0, 1, 2, 3, 5, 4]
console.groupEnd();
console.group('任意数组项向后移一位');
console.log(arr.index(0).moveTo('backward')); // [1, 0, 2, 3, 4, 5]
console.log(arr.index(3).moveTo('backward')); // [0, 1, 2, 4, 3, 5]
console.log(arr.index(5).moveTo('backward')); // [0, 1, 2, 3, 4, 5]
console.groupEnd();
console.group('将对应数组项移动到最前面');
console.log(arr.index(2).moveTo(0)); // [2, 0, 1, 3, 4, 5]
console.log(arr.index(1).moveTo(-10)); // [1, 0, 2, 3, 4, 5]
console.log(arr.index(5).moveTo('start')); // [5, 0, 1, 2, 3, 4]
console.groupEnd();
console.group('将对应数组项移动到最后面');
console.log(arr.index(2).moveTo(arr.length - 1)); // [0, 1, 3, 4, 5, 2]
console.log(arr.index(1).moveTo(100)); // [0, 2, 3, 4, 5, 1]
console.log(arr.index(3).moveTo('end')); // [0, 1, 2, 4, 5, 3]
console.groupEnd();
console.group('任意两个数组项交换位置');
console.log(arr.index(0).swapWith(5)); // [5, 1, 2, 3, 4, 0]
console.log(arr.index(3).swapWith(1)); // [0, 3, 2, 1, 4, 5]
console.log(arr.index(4).swapWith(5)); // [0, 1, 2, 3, 5, 4]
console.groupEnd();
console.group('边界情况验证');
// 目标索引值超出数组范围,按数组边界处理
console.log(arr.index(2).moveTo(-1)); // [2, 0, 1, 3, 4, 5]
console.log(arr.index(2).moveTo(100)); // [0, 1, 3, 4, 5, 2]
// 字符串数字按数字处理
console.log(arr.index('2').moveTo('123')); // [0, 1, 3, 4, 5, 2]
console.log(arr.index('3').swapWith('4')); // [0, 1, 2, 4, 3, 5]
// 非法索引值,报错
// Error: `abc` is not a integer.
console.log(arr.index('abc').moveTo(4));
console.log(arr.index(4).moveTo('abc'));
console.log(arr.index(4).swapWith('abc'));
// 索引元素不存在,报错
// Error: The index(`100`) of item is not in the array([0,1,2,3,4,5])
// Error: The index(`-2`) of item is not in the array([0,1,2,3,4,5])
console.log(arr.index(100).moveTo(5));
console.log(arr.index(5).swapWith(100));
console.log(arr.index(5).swapWith(-2));
// 非数组调用,报错(JS内部报语法错误)
// Error: "123".index is not a function
console.log('123'.index(2).swapWith(5));
console.groupEnd();
我的回答:用 jQuery 的 closest()
方法。开个玩笑 😆
先说下思路:根据传入的参数,使用 querySelectorAll()
方法找出所有匹配的选择器,再匹配出包含当前元素的 nodeList 列表。
第一题依赖这个方法,所以它先上
Element.prototype.closestAll = function(targetEl) {
var el = this;
var nodelist;
// 参数校验
if (typeof targetEl !== 'string' || !targetEl.trim()) {
throw Error('\' + targetEl + \' is not a valid selector');
}
nodelist = document.querySelectorAll(targetEl);
// 使用 ES5 的 filter 过滤出包含 el 的元素
return [].slice.call(nodelist)
.filter(function(node) {
return node.contains(el)
})
.reverse(); // 反转数组,最近的排前面,依次从近到远
}
获取
closestAll()
方法返回的第一项即可
// closest polyfill
window.Element && 'closest' in Element.prototype || +function() {
Element.prototype.closest = function(targetEl) {
var result = this.closestAll(targetEl);
return result.length === 0 ? null : result[0];
}
}();
Chrome | Firefox | IE11 | |
---|---|---|---|
0.6.toFixed(0) |
1 | 1 | 1 |
1.6.toFixed(0) |
2 | 2 | 2 |
0.035.toFixed(2) |
0.04 | 0.04 | 0.04 |
0.045.toFixed(2) |
0.04 | 0.04 | 0.05 |
Number.prototype.toFixed = myToFixed;
String.prototype.toFixed = myToFixed;
function myToFixed(digits = 0) {
let num = String(this);
// 小数位不足时后置补零
const postfixZero = s => {
let len = digits;
if (s.indexOf('.') > 0) {
len = digits - s.split('.')[1].length;
} else if (len > 0) {
s += '.'; // 整数位后补小数点
}
return s + '0'.repeat(len);
};
digits = parseInt(digits, 10);
if (isNaN(digits)) { // 非数值
throw new Error('digits argument must be a number(or string number)');
}
if (digits < 0) { // 负数
throw new Error('digits argument must be greater than or equal to 0');
}
if (!isFinite(num)) { // +-Infinity
return num;
}
if (num.indexOf('.') > 0) {
let times = Math.pow(10, digits); // 根据小数位长度获取升幂倍数
num = Math.round(num * times) / times + ''; // 四舍五入,降幂
}
return postfixZero(num);
}
测试用例:
console.group('普通场景测试');
console.log(0.6.toFixed(0)); // "1"
console.log(1.6.toFixed(0)); // "2"
console.log(0.035.toFixed(2)); // "0.04"
console.log(0.045.toFixed(2)); // "0.05"
console.groupEnd();
console.group('进阶场景测试');
console.log(Math.PI.toFixed(10)); // "3.1415926536"
console.log(0.9.toFixed()); // "1"
console.log(Number(5).toFixed(2)); // "5.00"
console.log(3..toFixed(3)); // "3.000"
console.log(.5.toFixed('3')); // "0.500"
console.log(1.2345.toFixed(2.6)); // "1.23"
console.groupEnd();
console.group('参数异常测试');
console.log(Infinity.toFixed(5)); // "Infinity"
console.log(0.5.toFixed(-2)); // Error: digits argument must be greater than or equal to 0
console.log(0.5.toFixed(null)); // Error: digits argument must be a number(or string number)
console.groupEnd();
//zxz: 滴~满分~
<ol class="steps">
<li>1-规则说明</li>
<li>2-参与活动</li>
<li class="is-current">3-参与抽奖</li>
<li>4-奖品发放</li>
<li>5-查看结果</li>
</ol>
.steps {
display: flex;
padding: 0; margin: 0;
list-style: none;
}
.steps > li {
display: flex;
align-items: center;
height: 40px;
padding: 0 2em;
margin-left: -13px; /* 15px - 13px = 2px(间隙) */
font-size: 14px;
color: #009fe9;
clip-path: polygon(
0 0, calc(100% - 15px) 0,
100% 50%, 100% 50%,
calc(100% - 15px) 100%, 0 100%,
15px 50%
);
background: #edf9ff;
}
.steps > li:first-child {
margin-left: 0;
clip-path: polygon(
0 0, calc(100% - 15px) 0,
100% 50%, 100% 50%,
calc(100% - 15px) 100%, 0 100%
);
}
.steps > .is-current {
color: #fff;
background: #009fe9;
}
.steps > .is-current ~ li {
color: #8c8c8c;
background: #ebedf0;
}
<!-- HTML 结构同方案一 -->
.steps {
display: table;
border-collapse: separate;
border-spacing: 30px 0;
padding: 0; margin: 0;
list-style: none;
}
.steps > li {
position: relative;
display: table-cell;
height: 40px;
padding: 0 1em 0 .5em;
vertical-align: middle;
font-size: 14px;
color: #009fe9;
background: #edf9ff;
}
.steps > li::before,
.steps > li::after {
content: "";
position: absolute;
top: 0;
border: solid transparent;
border-width: 20px 14px; /* (15 - 14) × 2 = 2px(间隙) */
}
.steps > li::before {
left: 0;
transform: translateX(-100%);
border-color: #edf9ff;
border-left-color: transparent;
}
.steps > li::after {
right: 0;
transform: translateX(100%);
border-left-color: #edf9ff;
}
.steps > li:first-child::before {border-left-color: #edf9ff;}
.steps > .is-current {
color: #fff;
background: #009fe9;
}
.steps > .is-current::before {
border-color: #009fe9;
border-left-color: transparent;
}
.steps > .is-current:first-child:before {border-left-color: #009fe9;}
.steps > .is-current::after {border-left-color: #009fe9;}
.steps > .is-current ~ li {
color: #8c8c8c;
background: #ebedf0;
}
.steps > .is-current ~ li::before {
border-color: #ebedf0;
border-left-color: transparent;
}
.steps > .is-current ~ li::after {border-left-color: #ebedf0;}
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.