现代 JavaScript 教程任务题解二
鼠标拖放事件
任务
滑动条
创建一个滑动条(slider):
用鼠标拖动蓝色的滑块(thumb)并移动它。
重要的细节:
- 当鼠标按钮被按下时,在滑动过程中,鼠标指针可能会移动到滑块的上方或下方。此时滑块仍会继续移动(方便用户)。
- 如果鼠标非常快地向左边或者向右边移动,那么滑块应该恰好停在边缘。
const slider = document.querySelector('#slider'); const thumb = document.querySelector('.thumb'); const thumbWidth = thumb.clientWidth; const sliderLeft = slider.offsetLeft; const sliderWidth = slider.clientWidth; console.log(slider.clientWidth); thumb.onmousedown = function () { function moveAt(x) { let res = x - sliderLeft; if (x > sliderWidth) res = sliderWidth - thumbWidth; if (x < sliderLeft) res = sliderLeft - thumbWidth; thumb.style.left = res + 'px'; } function moveHandle(event) { moveAt(event.pageX); } document.addEventListener('mousemove', moveHandle); document.onmouseup = function () { console.log('here'); document.removeEventListener('mousemove', moveHandle); document.onmouseup = null; }; }; thumb.ondragstart = function () { return false; }; // 参考解法 let thumb = slider.querySelector('.thumb'); thumb.onmousedown = function(event) { event.preventDefault(); // prevent selection start (browser action) let shiftX = event.clientX - thumb.getBoundingClientRect().left; // shiftY not needed, the thumb moves only horizontally document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); function onMouseMove(event) { let newLeft = event.clientX - shiftX - slider.getBoundingClientRect().left; // the pointer is out of slider => lock the thumb within the bounaries if (newLeft < 0) { newLeft = 0; } let rightEdge = slider.offsetWidth - thumb.offsetWidth; if (newLeft > rightEdge) { newLeft = rightEdge; } thumb.style.left = newLeft + 'px'; } function onMouseUp() { document.removeEventListener('mouseup', onMouseUp); document.removeEventListener('mousemove', onMouseMove); } }; thumb.ondragstart = function() { return false; };
将超级英雄放置在足球场周围
这个任务可以帮助你检查你对拖放和 DOM 的一些方面的理解程度。
使所有元素都具有类
draggable
—— 可拖动。就像本章中的球一样。要求:
- 使用事件委托来跟踪拖动的开始:一个在
document
上的用于mousedown
的处理程序。 - 如果元素被拖动到了窗口的顶端/末端 —— 页面会向上/向下滚动以允许进一步的拖动。
- 没有水平滚动(这使本任务更简单,但添加水平滚动也很简单)。
- 即使在快速移动鼠标后,可拖动元素或该元素的部分也绝不应该离开窗口。
这个示例太大了,不适合放在这里,所以在下面给出了示例链接。
// 参考解法 let isDragging = false; document.addEventListener('mousedown', function(event) { let dragElement = event.target.closest('.draggable'); if (!dragElement) return; event.preventDefault(); dragElement.ondragstart = function() { return false; }; let coords, shiftX, shiftY; startDrag(dragElement, event.clientX, event.clientY); function onMouseUp(event) { finishDrag(); }; function onMouseMove(event) { moveAt(event.clientX, event.clientY); } // 在拖动开始时: // 记住初始的移位 // 将元素设置为 position:fixed,并将此元素移动到作为 body 的直接子元素 function startDrag(element, clientX, clientY) { if(isDragging) { return; } isDragging = true; document.addEventListener('mousemove', onMouseMove); element.addEventListener('mouseup', onMouseUp); shiftX = clientX - element.getBoundingClientRect().left; shiftY = clientY - element.getBoundingClientRect().top; element.style.position = 'fixed'; moveAt(clientX, clientY); }; // 在最后,转换到绝对(absolute)坐标,以将元素固定在文档中 function finishDrag() { if(!isDragging) { return; } isDragging = false; dragElement.style.top = parseInt(dragElement.style.top) + window.pageYOffset + 'px'; dragElement.style.position = 'absolute'; document.removeEventListener('mousemove', onMouseMove); dragElement.removeEventListener('mouseup', onMouseUp); } function moveAt(clientX, clientY) { // 新的窗口相对坐标 let newX = clientX - shiftX; let newY = clientY - shiftY; // 检查新坐标是否在底部窗口边缘以下 let newBottom = newY + dragElement.offsetHeight; // new bottom // 在窗口边缘以下?让我们滚动此页面 if (newBottom > document.documentElement.clientHeight) { // 文档末端的窗口相对坐标 let docBottom = document.documentElement.getBoundingClientRect().bottom; // 将文档向下滚动 10px 有一个问题 // 它可以滚动到文档末尾之后 // Math.min(how much left to the end, 10) let scrollY = Math.min(docBottom - newBottom, 10); // 计算是不精确的,可能会有舍入误差导致页面向上滚动 // 这是不应该出现,我们在这儿解决它 if (scrollY < 0) scrollY = 0; window.scrollBy(0, scrollY); // 快速移动鼠标将指针移至文档末端的外面 // 如果发生这种情况 —— // 使用最大的可能距离来限制 newY(就是文档末端到顶端的距离) newY = Math.min(newY, document.documentElement.clientHeight - dragElement.offsetHeight); } // 检查新坐标是否在顶部窗口边缘上方(类似的逻辑) if (newY < 0) { // scroll up let scrollY = Math.min(-newY, 10); if (scrollY < 0) scrollY = 0; // 检查精度损失 window.scrollBy(0, -scrollY); // 快速移动鼠标可以使指针超出文档的顶端 newY = Math.max(newY, 0); // newY 不得小于 0 } // 将 newX 限制在窗口范围内 // 这里没有滚动,所以它很简单 if (newX < 0) newX = 0; if (newX > document.documentElement.clientWidth - dragElement.offsetWidth) { newX = document.documentElement.clientWidth - dragElement.offsetWidth; } dragElement.style.left = newX + 'px'; dragElement.style.top = newY + 'px'; } });
- 使用事件委托来跟踪拖动的开始:一个在
键盘:keydown 和 keyup
任务
扩展热键
创建一个
runOnKeys(func, code1, code2, ... code_n)
函数,在同时按下code1, code2, ... code_n
键时运行函数func
。例如,当按键
"Q"
和"W"
被一起按下时(任何语言中,无论是否 CapsLock),下面的代码将显示alert
:runOnKeys( () => alert("Hello!"), "KeyQ", "KeyW" );
// 参考解法 function runOnKeys(func, ...codes) { let pressed = new Set(); document.addEventListener('keydown', function(event) { pressed.add(event.code); for (let code of codes) { // 所有的按键都在集合中? if (!pressed.has(code)) { return; } } // 是的 // 在 alert 期间,如果访客松开了按键, // JavaScript 就不会获得 "keyup" 事件 // 那么集合 pressed 会保持假设这些按键是被按下的状态 // 因此,为避免“粘滞”键,我们对状态进行了重置 // 如果用户想再次运行热键 —— 他们需要再次按下所有键 pressed.clear(); func(); }); document.addEventListener('keyup', function(event) { pressed.delete(event.code); }); } runOnKeys( () => alert("Hello!"), "KeyQ", "KeyW" );
滚动
任务
无限的页面
创建一个无限的页面。当访问者滚动到页面末端时,它会自动将当期日期时间附加到文本中(以便访问者可以滚动更多内容)。
像这样:
请注意滚动的两个重要特性:
- 滚动是“弹性的”。在某些浏览器/设备中,我们可以在文档的顶端或末端稍微多滚动出一点(超出部分显示的是空白区域,然后文档将自动“弹回”到正常状态)。
- 滚动并不精确。当我们滚动到页面末端时,实际上我们可能距真实的文档末端约 0-50px。
因此,“滚动到末端”应该意味着访问者离文档末端的距离不超过 100px。
P.S. 在现实生活中,我们可能希望显示“更多信息”或“更多商品”。
function createDate() { const p = document.createElement('p'); p.innerHTML = new Date(); document.body.append(p); } function addScroll() { let { scrollTop, scrollHeight, clientHeight } = document.documentElement; while (scrollTop + clientHeight >= scrollHeight - 100) { createDate(); ({ scrollTop, scrollHeight, clientHeight } = document.documentElement); // 添加元素后要更新相应的值,否则会造成死循环。 } } addScroll(); document.addEventListener('scroll', event => { addScroll(); }); // 参考解法 function populate() { while(true) { let windowRelativeBottom = document.documentElement.getBoundingClientRect().bottom; if (windowRelativeBottom > document.documentElement.clientHeight + 100) break; document.body.insertAdjacentHTML("beforeend", `<p>Date: ${new Date()}</p>`); } } window.addEventListener('scroll', populate); populate(); // init document
Up/down 按钮
创建一个“到顶部”按钮来帮助页面滚动。
它应该像这样运行:
- 页面向下滚动的距离没有超过窗口高度时 —— 按钮不可见。
- 当页面向下滚动距离超过窗口高度时 —— 在左上角出现一个“向上”的箭头。如果页面回滚回去,箭头就会消失。
- 单击箭头时,页面将滚动到顶部。
像这样(左上角,滚动查看):
#arrowTop { // ... display: none; } // ... #arrowTop.show { display: block; }
const arrow = document.querySelector('#arrowTop'); function checkArrow() { const { scrollTop, clientHeight } = document.documentElement; if (scrollTop >= clientHeight) arrow.classList.add('show'); else arrow.classList.remove('show'); } arrow.addEventListener('click', event => { document.documentElement.scrollTo(0, 0); }); document.addEventListener('scroll', checkArrow); // 参考解法 arrowTop.onclick = function() { window.scrollTo(pageXOffset, 0); // after scrollTo, there will be a "scroll" event, so the arrow will hide automatically }; window.addEventListener('scroll', function() { arrowTop.hidden = (pageYOffset < document.documentElement.clientHeight); });
加载可视化图像
假设我们有一个速度较慢的客户端,并且希望节省它们在移动端的流量。
为此,我们决定不立即显示图像,而是将其替换为占位符,如下所示:
<img src="placeholder.svg" width="128" height="128" data-src="real.jpg">
因此,最初所有图像均为
placeholder.svg
。当页面滚动到用户可以看到图像位置时 —— 我们就会将src
更改为data-src
的src
,从而加载图像。这是在
iframe
中的一个示例:滚动它可以看到图像是“按需”加载的。
要求:
- 加载页面时,屏幕上的那些图像应该在滚动之前立即加载。
- 有些图像可能是常规图像,没有
data-src
。代码不应该改动它们。 - 一旦图像被加载,它就不应该在滚动进/出时被重新加载。
P.S. 如果你有能力,可以创建一个更高级的解决方案,以“预加载”当前位置下方/之后一页的图像。
P.P.S. 仅处理垂直滚动,不处理水平滚动。
/** * Tests if the element is visible (within the visible part of the page) * It's enough that the top or bottom edge of the element are visible */ function isVisible(elem) { // todo: your code const { offsetTop, height } = elem; const { scrollTop, clientHeight } = document.documentElement; console.log(offsetTop, height, scrollTop, clientHeight); return offsetTop + height <= scrollTop + clientHeight; } function showVisible() { for (let img of document.querySelectorAll('img')) { let realSrc = img.dataset.src; if (!realSrc) continue; if (isVisible(img)) { // disable caching // this line should be removed in production code realSrc += '?nocache=' + Math.random(); img.src = realSrc; img.dataset.src = ''; } } } window.addEventListener('scroll', showVisible); showVisible(); // 参考解法 1 function isVisible(elem) { let coords = elem.getBoundingClientRect(); let windowHeight = document.documentElement.clientHeight; // top elem edge is visible OR bottom elem edge is visible let topVisible = coords.top > 0 && coords.top < windowHeight; let bottomVisible = coords.bottom < windowHeight && coords.bottom > 0; return topVisible || bottomVisible; } // 参考解法 2 function isVisible(elem) { let coords = elem.getBoundingClientRect(); let windowHeight = document.documentElement.clientHeight; let extendedTop = -windowHeight; let extendedBottom = 2 * windowHeight; // top visible || bottom visible let topVisible = coords.top > extendedTop && coords.top < extendedBottom; let bottomVisible = coords.bottom < extendedBottom && coords.bottom > extendedTop; return topVisible || bottomVisible; }
表单属性和方法
任务
在 select 元素中添加一个选项
下面是一个
<select>
元素:<select id="genres"> <option value="rock">Rock</option> <option value="blues" selected>Blues</option> </select>
使用 JavaScript 来实现:
- 显示所选选项的值和文本。
- 添加一个选项:
<option value="classic">Classic</option>
。 - 使之变为可选的。
请注意,如果你已正确完成所有事项,那么
alert
应该显示blues
。// 解决方案 // 1) let selectedOption = genres.options[genres.selectedIndex]; alert( selectedOption.value ); // 2) let newOption = new Option("Classic", "classic"); genres.append(newOption); // 3) newOption.selected = true;
聚焦:focus/blur
任务
可编辑的 div
创建一个
<div>
,它在被点击后变成<textarea>
。文本区域(textarea)允许我们编辑
<div>
里的 HTML。当用户按下 Enter 键,或者
<textarea>
失去焦点时,<textarea>
会变回<div>
,并且<textarea>
中的内容会变成<div>
中的 HTML。const view = document.querySelector('#view'); view.addEventListener('click', () => { const textarea = document.createElement('textarea'); textarea.value = view.innerHTML; textarea.classList.add('edit'); textarea.addEventListener('blur', () => { remove(textarea); }); textarea.addEventListener('keydown', e => { if (e.keyCode !== 13) return; textarea.blur(); }); document.body.append(textarea); textarea.focus(); view.style.display = 'none'; }); function remove(textarea) { view.style.display = 'block'; view.innerHTML = textarea.value; textarea.remove(); } // 解决方案 let area = null; let view = document.getElementById('view'); view.onclick = function() { editStart(); }; function editStart() { area = document.createElement('textarea'); area.className = 'edit'; area.value = view.innerHTML; area.onkeydown = function(event) { if (event.key == 'Enter') { this.blur(); } }; area.onblur = function() { editEnd(); }; view.replaceWith(area); area.focus(); } function editEnd() { view.innerHTML = area.value; area.replaceWith(view); }
点击即可编辑单元格
使单元格在点击时可编辑。
- 点击时 —— 单元格应该变成“可编辑的”(在里面会出现文本区域),我们修改其中的 HTML。在这不调整单元格大小,所有几何形状保持不变。
- OK 和 CANCEL 按钮会出现在单元格的下面,用以完成/取消编辑。
- 同一时刻只有一个单元格可被编辑。当一个
<td>
处于“编辑模式”时,在其它单元格上的点击会被忽略。 - 该表格可能有很多单元格。请使用事件委托。
示例:
.edit-td { position: relative; padding: 0; } .edit { display: block; overflow: auto; width: 170px; height: 104px; padding: 0; margin: 0; outline: none; border: none; resize: none; } .btn-group { position: absolute; top: 100%; }
let table = document.getElementById('bagua-table'); let edit = null; let tdContent = ''; table.addEventListener('click', e => { const target = e.target.closest('td'); if (!target) return; edit || editStart(target); }); function editStart(td) { edit = document.createElement('textarea'); edit.value = td.innerHTML; edit.classList.add('edit'); const btn = ` <button data-type="1">ok</button> <button data-type="2">cancel</button> `; const div = document.createElement('div'); div.addEventListener('click', e => { const target = e.target; const type = target.dataset.type; if (type === '1') { confirmEdit(td, edit); } else { cancelEdit(td); } }); div.classList.add('btn-group'); div.innerHTML = btn; tdContent = td.innerHTML; td.innerHTML = ''; td.append(edit); td.append(div); td.classList.add('edit-td'); } function cancelEdit(td) { td.innerHTML = tdContent; tdContent = ''; edit = null; } function confirmEdit(td, edit) { td.innerHTML = edit.value; tdContent = ''; edit = null; }
参考解法:
.edit-td .edit-area { border: none; margin: 0; padding: 0; display: block; /* remove resizing handle in Firefox */ resize: none; /* remove outline on focus in Chrome */ outline: none; /* remove scrollbar in IE */ overflow: auto; } .edit-controls { position: absolute; } .edit-td { position: relative; padding: 0; }
let table = document.getElementById('bagua-table'); let editingTd; table.onclick = function(event) { // 3 possible targets let target = event.target.closest('.edit-cancel,.edit-ok,td'); if (!table.contains(target)) return; if (target.className == 'edit-cancel') { finishTdEdit(editingTd.elem, false); } else if (target.className == 'edit-ok') { finishTdEdit(editingTd.elem, true); } else if (target.nodeName == 'TD') { if (editingTd) return; // already editing makeTdEditable(target); } }; function makeTdEditable(td) { editingTd = { elem: td, data: td.innerHTML }; td.classList.add('edit-td'); // td is in edit state, CSS also styles the area inside let textArea = document.createElement('textarea'); textArea.style.width = td.clientWidth + 'px'; textArea.style.height = td.clientHeight + 'px'; textArea.className = 'edit-area'; textArea.value = td.innerHTML; td.innerHTML = ''; td.appendChild(textArea); textArea.focus(); td.insertAdjacentHTML("beforeEnd", '<div class="edit-controls"><button class="edit-ok">OK</button><button class="edit-cancel">CANCEL</button></div>' ); } function finishTdEdit(td, isOk) { if (isOk) { td.innerHTML = td.firstChild.value; } else { td.innerHTML = editingTd.data; } td.classList.remove('edit-td'); editingTd = null; }
键盘移动老鼠
聚焦在老鼠上。然后使用键盘的方向键移动它:
P.S. 除了
#mouse
元素外,不要在任何地方放置事件处理程序。 P.P.S. 不要修改 HTML/CSS,这个方法应该是通用的,可以用于任何元素。const mouse = document.querySelector('#mouse'); mouse.tabIndex = 0; mouse.onclick = function () { mouse.focus(); mouse.style.position = 'fixed'; mouse.style.left = mouse.offsetLeft + 'px'; mouse.style.top = mouse.offsetTop + 'px'; mouse.onkeydown = function (event) { switch (event.key) { case 'ArrowLeft': this.style.left = parseInt(this.style.left) - this.offsetWidth + 'px'; return false; case 'ArrowUp': this.style.top = parseInt(this.style.top) - this.offsetHeight + 'px'; return false; case 'ArrowRight': this.style.left = parseInt(this.style.left) + this.offsetWidth + 'px'; return false; case 'ArrowDown': this.style.top = parseInt(this.style.top) + this.offsetHeight + 'px'; return false; } }; };
表单:事件和方法提交
任务
模态框表单
创建一个函数
showPrompt(html, callback)
,该函数显示一个表单,里面有消息html
,一个input
字段和OK/CANCEL
按钮。- 用户应该在文本字段中输入一些内容,然后按下 Enter 键或点击 OK 按钮,然后
callback(value)
就会被调用,参数为输入的值。 - 否则,如果用户按下 Esc 键或点击 CANCEL 按钮,那么
callback(null)
就会被调用。
在这两种情况下,输入过程都会结束,并移除表单。
要求:
- 表单应该在窗口的正中心。
- 表单是 模态框(modal)。换句话说,在用户关闭模态框之前,用户无法与页面的其它部分进行任何交互。
- 当表单显示后,焦点应该在用户需要进行输入的
<input>
输入框中。 - 按键 Tab/Shift+Tab 应该能在表单字段之间切换焦点,不允许焦点离开表单字段到页面的其它元素上。
使用示例:
showPrompt("Enter something<br>...smart :)", function(value) { alert(value); });
使用 iframe 嵌入的一个示例:
P.S. 源文档有给表单设定了固定位置的 HTML/CSS,但是做成模态框的方式取决于你。
.hidden { display: none; }
<button id="test">click</button> <div id="prompt-form-container" class="hidden"> <!-- ... --> </div>
const test = document.querySelector('#test'); const message = document.querySelector('#prompt-message'); const promtpFormContainer = document.querySelector( '#prompt-form-container' ); const promptForm = document.querySelector('#prompt-form'); promptForm.onsubmit = function () { return false; }; test.onclick = function () { showPrompt('Enter something<br>...smart', function (value) { alert(value); }); // promtpFormContainer.classList.remove('hidden'); }; function showPrompt(text, callback) { promtpFormContainer.classList.remove('hidden'); message.innerHTML = text; promptForm.ok.onclick = function () { callback(promptForm.text.value); promtpFormContainer.classList.add('hidden'); }; promptForm.cancel.onclick = function () { callback(null); promtpFormContainer.classList.add('hidden'); }; }
参考解法:
#cover-div { position: fixed; top: 0; left: 0; z-index: 9000; width: 100%; height: 100%; background-color: gray; opacity: 0.3; }
<h2>Click the button below</h2> <input type="button" value="Click to show the form" id="show-button">
// Show a half-transparent DIV to "shadow" the page // (the form is not inside, but near it, because it shouldn't be half-transparent) function showCover() { let coverDiv = document.createElement('div'); coverDiv.id = 'cover-div'; // make the page unscrollable while the modal form is open document.body.style.overflowY = 'hidden'; document.body.append(coverDiv); } function hideCover() { document.getElementById('cover-div').remove(); document.body.style.overflowY = ''; } function showPrompt(text, callback) { showCover(); let form = document.getElementById('prompt-form'); let container = document.getElementById('prompt-form-container'); document.getElementById('prompt-message').innerHTML = text; form.text.value = ''; function complete(value) { hideCover(); container.style.display = 'none'; document.onkeydown = null; callback(value); } form.onsubmit = function() { let value = form.text.value; if (value == '') return false; // ignore empty submit complete(value); return false; }; form.cancel.onclick = function() { complete(null); }; document.onkeydown = function(e) { if (e.key == 'Escape') { complete(null); } }; let lastElem = form.elements[form.elements.length - 1]; let firstElem = form.elements[0]; lastElem.onkeydown = function(e) { if (e.key == 'Tab' && !e.shiftKey) { firstElem.focus(); return false; } }; firstElem.onkeydown = function(e) { if (e.key == 'Tab' && e.shiftKey) { lastElem.focus(); return false; } }; container.style.display = 'block'; form.elements.text.focus(); } document.getElementById('show-button').onclick = function() { showPrompt("Enter something<br>...smart :)", function(value) { alert("You entered: " + value); }); };
- 用户应该在文本字段中输入一些内容,然后按下 Enter 键或点击 OK 按钮,然后
资源加载:onload, onerror
任务
使用回调函数加载图片
通常,图片在被创建时才会被加载。所以,当我们向页面中添加
<img>
时,用户不会立即看到图片。浏览器首先需要加载它。为了立即显示一张图片,我们可以“提前”创建它,像这样:
let img = document.createElement('img'); img.src = 'my.jpg';
浏览器开始加载图片,并将其保存到缓存中。以后,当相同图片出现在文档中时(无论怎样),它都会立即显示。
创建一个函数
preloadImages(sources, callback)
,来加载来自数组source
的所有图片,并在准备就绪时运行callback
。例如,这段代码将在图片加载完成后显示一个
alert
:function loaded() { alert("Images loaded") } preloadImages(["1.jpg", "2.jpg", "3.jpg"], loaded);
如果出现错误,函数应该仍假定图片已经“加载完成”。
换句话说,当所有图片都已加载完成,或出现错误输出时,将执行
callback
。例如,当我们计划显示一个包含很多图片的可滚动图册,并希望确保所有图片都已加载完成时,这个函数很有用。
在源文档中,你可以找到指向测试图片的链接,以及检查它们是否已加载完成的代码。它应该输出
300
。function preloadImages(sources: string[], callback: () => void) { const list = sources.map((src) => { return new Promise((resolve, reject) => { let img = document.createElement('img'); img.src = src; img.onload = resolve; img.onerror = reject; }); }); Promise.all(list).then(callback).catch(callback); } // 参考解法 function preloadImages(sources, callback) { let counter = 0; function onLoad() { counter++; if (counter == sources.length) callback(); } for(let source of sources) { let img = document.createElement('img'); img.onload = img.onerror = onLoad; img.src = source; } }
ArrayBuffer,二进制数组
任务
拼接类型化数组
给定一个
Uint8Array
数组,请写一个函数concat(arrays)
,将数组拼接成一个单一数组并返回。function concat(arrays) { // ...your code... const len = arrays.reduce((previous, current) => { return previous + current.length; }, 0); const res = new Uint8Array(len); arrays.forEach((value, index) => { res.set(value, index * value.length); }); return res; } // 参考解法 function concat(arrays) { // sum of individual array lengths let totalLength = arrays.reduce((acc, value) => acc + value.length, 0); if (!arrays.length) return null; let result = new Uint8Array(totalLength); // for each array - copy it over result // next array is copied right after the previous one let length = 0; for(let array of arrays) { result.set(array, length); length += array.length; } return result; }
Fetch
任务
从 GitHub fetch 用户信息
创建一个异步函数
getUsers(names)
,该函数接受 GitHub 登录名数组作为输入,查询 GitHub 以获取有关这些用户的信息,并返回 GitHub 用户数组。带有给定
USERNAME
的用户信息的 GitHub 网址是:https://api.github.com/users/USERNAME
。沙箱中有一个测试用例。
重要的细节:
- 对每一个用户都应该有一个
fetch
请求。 - 请求不应该相互等待。以便能够尽快获取到数据。
- 如果任何一个请求失败了,或者没有这个用户,则函数应该返回
null
到结果数组中。
async function getUsers(names) { const list = names.map(name => { return fetch(`https://api.github.com/users/${name}`); }); const res = await Promise.all(list); return await Promise.all(res.map(item => { if (item.status === 404) return null; const data = item.json(); return data; })); } // 参考解法 async function getUsers(names) { let jobs = []; for(let name of names) { let job = fetch(`https://api.github.com/users/${name}`).then( successResponse => { if (successResponse.status != 200) { return null; } else { return successResponse.json(); } }, failResponse => { return null; } ); jobs.push(job); } let results = await Promise.all(jobs); return results; } /** 请注意:.then 调用紧跟在 fetch 后面,这样,当我们收到响应时,它不会等待其他的 fetch,而是立即开始读取 .json()。 如果我们使用 await Promise.all(names.map(name => fetch(...))),并在 results 上调用 .json() 方法,那么它将会等到所有 fetch 都获取到响应数据才开始解析。通过将 .json() 直接添加到每个 fetch 中,我们就能确保每个 fetch 在收到响应时都会立即开始以 JSON 格式读取数据,而不会彼此等待。 这个例子表明,即使我们主要使用 async/await,低级别的 Promise API 仍然很有用。 */
- 对每一个用户都应该有一个
LocalStorage, sessionStorage
任务
自动保存表单字段
创建一个
textarea
字段,每当其值发生变化时,可以将其“自动保存”。因此,如果用户不小心关闭了页面,然后重新打开,他会发现之前未完成的输入仍然保留在那里。
像这样:
area.value = localStorage.getItem('area'); area.oninput = () => { localStorage.setItem('area', area.value) };
CSS 动画
任务
让飞机动起来 (CSS)
生成如下图的动画(点击显示):
- 点击后,图片会从
40x24px
变为400x240px
(变大十倍)。 - 动画持续三秒。
- 在动画结束后,输出:“Done!”。
- 动画过程中,如果飞机被点击,这些操作不应该打断动画。
#flyjet.animate { width: 400px; height: 240px; transition: width 3s, height 3s; }
flyjet.onclick = function () { flyjet.classList.add('animate'); } flyjet.addEventListener('transitionend', function () { alert('done') })
transitionend
是根据属性来触发,设置了两个属性所以会触发两次。// 参考解法 flyjet.onclick = function() { let ended = false; flyjet.addEventListener('transitionend', function() { if (!ended) { ended = true; alert('Done!'); } }); flyjet.classList.add('growing'); }
- 点击后,图片会从
为飞机生成动画 (CSS)
修改前一个的任务 让飞机动起来(CSS) 的解决方案,让飞机超过原有的大小
400x240px
(跳脱出来),然后再回到之前的大小。这里是效果演示(点击飞机):
在前一个解决方案的基础上做修改。
#flyjet.animate { width: 400px; height: 240px; transition: width 3s, height 3s; transition-timing-function: cubic-bezier(0, 0, 0.75, 2); } /* 参考解法 */ #flyjet { width: 40px; height: 24px; transition: all 3s cubic-bezier(0.25, 1.5, 0.75, 1.5); }
圆圈动画
创建一个函数:
showCircle(cx, cy, radius)
,来显示一个不断变大的圆。cx,cy
为圆心相对于窗口的位置。radius
为圆的半径。
点击下方的按钮以演示效果:
源文件中提供了一个具有合适样式的圆样例,因此你需要做的就是创建合适的动画。
<button onclick="showCircle(150, 150, 100)">showCircle(150, 150, 100)</button>
.circle { transition-property: width, height, margin-left, margin-top; transition-duration: 2s; position: fixed; transform: translateX(-50%) translateY(-50%); background-color: red; border-radius: 50%; width: 0; height: 0; } .circle.grow { width: 200px; height: 200px; top: 150px; left: 150px; }
function showCircle() { document.querySelector('.circle').classList.add('grow') }
// 参考解法 function showCircle(cx, cy, radius) { let div = document.createElement('div'); div.style.width = 0; div.style.height = 0; div.style.left = cx + 'px'; div.style.top = cy + 'px'; div.className = 'circle'; document.body.append(div); setTimeout(() => { div.style.width = radius * 2 + 'px'; div.style.height = radius * 2 + 'px'; }, 0); }
JavaScript 动画
任务
为弹跳的球设置动画
做一个弹跳的球。点击查看应有的效果:
// 参考解法 function makeEaseOut(timing) { return function(timeFraction) { return 1 - timing(1 - timeFraction); } } function bounce(timeFraction) { for (let a = 0, b = 1, result; 1; a += b, b /= 2) { if (timeFraction >= (7 - 4 * a) / 11) { return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2) } } } ball.onclick = function() { let to = field.clientHeight - ball.clientHeight; animate({ duration: 2000, timing: makeEaseOut(bounce), draw(progress) { ball.style.top = to * progress + 'px' } }); };
设置动画使球向右移动
让球向右移动。像这样:
编写动画代码。终止时球到左侧的距离是
100px
。从前一个任务 为弹跳的球设置动画 的答案开始。
// 参考解法 function makeEaseOut(timing) { return function(timeFraction) { return 1 - timing(1 - timeFraction); } } function bounce(timeFraction) { for (let a = 0, b = 1, result; 1; a += b, b /= 2) { if (timeFraction >= (7 - 4 * a) / 11) { return -Math.pow((11 - 6 * a - 11 * timeFraction) / 4, 2) + Math.pow(b, 2) } } } function quad(timeFraction) { return Math.pow(timeFraction, 2); } ball.onclick = function() { let height = field.clientHeight - ball.clientHeight; let width = 100; animate({ duration: 2000, timing: makeEaseOut(bounce), draw: function(progress) { ball.style.top = height * progress + 'px' } }); animate({ duration: 2000, timing: makeEaseOut(quad), draw: function(progress) { ball.style.left = width * progress + "px" } }); }
Custom elements
任务
计时器元素实例
我们已经创建了
<time-formatted>
元素用于展示格式化好的时间。创建一个
<live-timer>
元素用于展示当前时间:- 这个元素应该在内部使用
<time-formatted>
,不要重复实现这个元素的功能。 - 每秒钟更新。
- 每一秒钟都应该有一个自定义事件
tick
被生成,这个事件的event.detail
属性带有当前日期。(参考章节 创建自定义事件 )。
使用方式:
<live-timer id="elem"></live-timer> <script> elem.addEventListener('tick', event => console.log(event.detail)); </script>
例子:
请注意:
- 在元素被从文档移除的时候,我们会清除
setInterval
的 timer。这非常重要,否则即使我们不再需要它了,它仍然会继续计时。这样浏览器就不能清除这个元素占用和被这个元素引用的内存了。 - 我们可以通过
elem.date
属性得到当前时间。类所有的方法和属性天生就是元素的方法和属性。
- 这个元素应该在内部使用
词边界:\b
任务
查找时间
时间的格式是:
hours:minutes
。小时和分钟都是两位数,如09:00
。编写正则表达式在字符串
Breakfast at 09:00 in the room 123:456.
中查找时间。P.S. 在这个任务里没有必要校验时间的正确性,所以
25:99
也可算做有效的结果。P.P.S. 正则表达式不能匹配
123:456
。const str = 'Breakfast at 09:00 in the room 123:456.'; const reg = /\b\d\d:\d\d\b/g; const res = str.match(reg); console.log(res);
集合和范围 […]
任务
Java[^script]
我们有一个正则表达式
/Java[^script]/
。它会和字符串
Java
中的任何一部分匹配吗?会和字符串JavaScript
任何一部分匹配吗?不匹配
Java
,匹配JavaS
。找到 hh: mm 或者 hh-mm 形式的时间字符串
时间可以通过
hours:minutes
或者hours-minutes
格式来表示。小时和分钟都有两个数字:09:00
或者21-30
。写一个正则表达式来找到时间:
let reg = /your regexp/g; alert( "Breakfast at 09:00. Dinner at 21-30".match(reg) ); // 09:00, 21-30
附:在这个任务中,我们假设时间总是正确的,并不需要过滤掉像 “45:67” 这样错误的时间字符串。稍后我们也会处理这个问题。
let reg = /\d\d[:-]\d\d/g;
量词 +,*,?
和 {n}
任务
如何找到省略号 “…”?
创建一个正则表达式来查找省略号:连续 3(或更多)个点。
例如:
let reg = /\.{3,}/g; alert( "Hello!... How goes?.....".match(reg) ); // ..., .....
针对 HTML 颜色的正则表达式
创建一个正则表达式来搜寻格式为
#ABCDEF
的 HTML 颜色值:首个字符#
以及接下来的六位十六进制字符。一个例子:
let reg = /#[0-9a-f]{6}\b/gi; let str = "color:#121212; background-color:#AA00ef bad-colors:f#fddee #fd2 #12345678"; alert( str.match(reg) ) // #121212,#AA00ef
P.S. 在这个任务中,我们不需要其他的颜色格式,比如
#123
或rgb(1,2,3)
等。
贪婪量词和惰性量词
任务
对于 /d+? d+?/ 的匹配
以下匹配的结果是什么?
alert( "123 456".match(/\d+? \d+?/g) ); // ?
结果是:
123 4
。首先,懒惰模式
\d+?
尝试去获取尽可能少的字符,但当它检测到空格,就得出匹配结果123
。然后,第二个
\d+?
就只获取一个字符,因为这就已足够了。查找 HTML 注释
找出文本中的所有注释:
let reg = /<!--[\s\S]*?-->/g; let str = `... <!-- My -- comment test --> .. <!----> .. `; alert( str.match(reg) ); // '<!-- My -- comment \n test -->', '<!---->'
寻找 HTML 标签
创建一个正则表达式语句来寻找所有具有其属性的(闭合或非闭合)HTML 标签。
用例:
let reg = /<[^<>]+>/g; // 参考解法 let reg = /<\w.*?>/g; let str = '<> <a href="/"> <input type="radio" checked> <b>'; alert( str.match(reg) ); // '<a href="/">', '<input type="radio" checked>', '<b>'
假设不包含
<
和>
(也包括引号),这将会简单许多。
捕获组
任务
Check MAC-address
MAC-address of a network interface consists of 6 two-digit hex numbers separated by a colon.
For instance:
'01:32:54:67:89:AB'
.Write a regexp that checks whether a string is MAC-address.
Usage:
let regexp = /([0-9a-f]{2}:){5}[0-9a-f]{2}/i; let regexp = /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/i; alert( regexp.test('01:32:54:67:89:AB') ); // true alert( regexp.test('0132546789AB') ); // false (no colons) alert( regexp.test('01:32:54:67:89') ); // false (5 numbers, must be 6) alert( regexp.test('01:32:54:67:89:ZZ') ) // false (ZZ ad the end)
Find color in the format #abc or #abcdef
Write a RegExp that matches colors in the format
#abc
or#abcdef
. That is:#
followed by 3 or 6 hexadecimal digits.Usage example:
let regexp = /#([0-9a-f]{3}){1,2}\b/gi; let str = "color: #3f3; background-color: #AA00ef; and: #abcd"; alert( str.match(regexp) ); // #3f3 #AA00ef
P.S. This should be exactly 3 or 6 hex digits. Values with 4 digits, such as
#abcd
, should not match.Find all numbers
Write a regexp that looks for all decimal numbers including integer ones, with the floating point and negative ones.
An example of use:
let regexp = /-?\d+(\.\d+)?/g; let str = "-1.5 0 2 -123.4."; alert( str.match(regexp) ); // -1.5, 0, 2, -123.4
Parse an expression
An arithmetical expression consists of 2 numbers and an operator between them, for instance:
1 + 2
1.2 * 3.4
-3 / -6
-2 - 2
The operator is one of:
"+"
,"-"
,"*"
or"/"
.There may be extra spaces at the beginning, at the end or between the parts.
Create a function
parse(expr)
that takes an expression and returns an array of 3 items:- The first number.
- The operator.
- The second number.
For example:
let [a, op, b] = parse("1.2 * 3.4"); alert(a); // 1.2 alert(op); // * alert(b); // 3.4 function parse(expression) { const reg = /\s*(?<first>\d+(\.\d+)?)\s*(?<op>[*+-/])\s*(?<second>\d+(\.\d+)?)\s*/; const { groups } = expression.match(reg); return [groups.first, groups.op, groups.second]; } // Groups that contain decimal parts (number 2 and 4) (.\d+) can be excluded by adding ?: to the beginning: (?:\.\d+)?. function parse(expr) { let regexp = /(-?\d+(?:\.\d+)?)\s*([-+*\/])\s*(-?\d+(?:\.\d+)?)/; let result = expr.match(regexp); if (!result) return []; result.shift(); return result; }
选择 (OR) |
任务
查找编程语言
有许多编程语言,例如 Java, JavaScript, PHP, C, C++。
构建一个正则式,用来匹配字符串
Java JavaScript PHP C++ C
中包含的编程语言:let reg = /Java(Script)?|PHP|C(\+\+)?/g; alert("Java JavaScript PHP C++ C".match(reg)); // Java JavaScript PHP C++ C
查找 bbtag 对
“bb-tag” 形如
[tag]...[/tag]
,tag
匹配b
、url
或quote
其中之一。例如:
[b]text[/b] [url]http://google.com[/url]
BB-tags 可以嵌套。但标签不能自嵌套,比如:
可行: [url] [b]http://google.com[/b] [/url] [quote] [b]text[/b] [/quote] 不可行: [b][b]text[/b][/b]
标签可以包含换行,通常为以下形式:
[quote] [b]text[/b] [/quote]
构造一个正则式用于查找所有 BB-tags 和其内容。
举例:
let reg = /your regexp/g; let str = "..[url]http://google.com[/url].."; alert( str.match(reg) ); // [url]http://google.com[/url]
如果标签嵌套,那么我们需要记录匹配的外层标签(如果希望继续查找匹配的标签内容的话):
let reg = /\[(b|url|quote)\][\s\S]*?\[\/\1\]/g; let str = "..[url][b]http://google.com[/b][/url].."; alert( str.match(reg) ); // [url][b]http://google.com[/b][/url]
查询引用字符串
构建一个正则表达式用于匹配双引号内的字符串
"..."
。最重要的部分是字符串应该支持转义,正如 JavaScript 字符串的行为一样。例如,引号可以插入为
\"
,换行符为\n
,斜杠本身为\\
。let str = "Just like \"here\".";
对我们来说,重要的是转义的引号
\"
不会结束字符串匹配。所以,我们应该匹配两个引号之间的内容,且忽略中间转义的引号。
这是任务的关键部分,否则这个任务就没什么意思了。
匹配字符串示例:
.. "test me" .. .. "Say \"Hello\"!" ... (escaped quotes inside) .. "\\" .. (double slash inside) .. "\\ \"" .. (double slash and an escaped quote inside)
在 JavaScript 中,双斜杠用于把斜杠转义为字符串,如下所示:
let reg = /"(\\.|[^"\\])*"/g; let str = ' .. "test me" .. "Say \\"Hello\\"!" .. "\\\\ \\"" .. '; alert( str.match(reg) ); // "test me","Say \"Hello\"!","\\ \""
查找完整标签
写出一个正则表达式,用于查找
<style...>
标签。它应该匹配完整的标签:该标签可能是没有属性的标签<style>
或是有很多属性的标签<style type="..." id="...">
。…同时正则表达式不应该匹配
<styler>
!举例如下:
let reg = /<style(>|\s.*?>)/g; alert( '<style> <styler> <style test="...">'.match(reg) ); // <style>, <style test="...">
前瞻断言与后瞻断言
任务
Find non-negative integers
There’s a string of integer numbers.
Create a regexp that looks for only non-negative ones (zero is allowed).
An example of use:
let regexp = /\b(?<!-)\d+\b/g; let regexp = /(?<![-\d])\d+/g; let str = "0 12 -5 123 -18"; alert( str.match(regexp) ); // 0, 12, 123
Вставьте после фрагмента
Есть строка с HTML-документом.
Вставьте после тега
<body>
(у него могут быть атрибуты) строку<h1>Hello</h1>
.Например:
let regexp = /ваше регулярное выражение/; let str = ` <html> <body style="height: 200px"> ... </body> </html> `; str = str.replace(regexp, `<h1>Hello</h1>`);
После этого значение
str
:<html> <body style="height: 200px"><h1>Hello</h1> ... </body> </html>
实在看不懂是啥……