现代 JavaScript 教程任务题解一
字符串
任务
首字母大写
写一个函数
ucFirst(str)
,并返回首字母大写的字符串str
,例如:ucFirst("john") == "John";
function ucFirst(str) { if (!str) return str; return str[0].toUpperCase() + str.slice(1); }
检查 spam
写一个函数
checkSpam(str)
,如果str
包含viagra
或XXX
就返回true
,否则返回false
。函数必须不区分大小写:
checkSpam('buy ViAgRA now') == true checkSpam('free xxxxx') == true checkSpam("innocent rabbit") == false
function checkSpam(str) { if (str.toLowerCase().includes('viagra') || str.toUpperCase().includes('XXX')) return true; return false; }
截断文本
创建函数
truncate(str, maxlength)
来检查str
的长度,如果超过maxlength
—— 应使用"…"
来代替str
的结尾部分,长度仍然等于maxlength
。函数的结果应该是截断后的文本(如果需要的话)。
例如:
truncate("What I'd like to tell on this topic is:", 20) = "What I'd like to te…" truncate("Hi everyone!", 20) = "Hi everyone!"
function truncate(str, maxlength) { if (str.length > maxlength) { return str.slice(0, maxlength - 1) + '…'; } return str; }
提取货币
我们有以
"$120"
这样的格式表示的花销。意味着:先是美元符号,然后才是数值。创建函数
extractCurrencyValue(str)
从字符串中提取数值并返回。例如:
alert( extractCurrencyValue('$120') === 120 ); // true
function extractCurrencyValue(str) { return +str.slice(1); }
数组
任务
数组被拷贝了吗?
下面的代码将会显示什么?
let fruits = ["Apples", "Pear", "Orange"]; // 在“副本”里 push 了一个新的值 let shoppingCart = fruits; shoppingCart.push("Banana"); // fruits 里面是什么? alert( fruits.length ); // ?
数组的赋值是引用赋值,所以输出是 4 。
数组操作
我们试试下面的 5 个数组操作。
- 创建一个数组
styles
,里面存储有 “Jazz” 和 “Blues”。 - 将 “Rock-n-Roll” 从数组末端添加进去。
- 用 “Classics” 替换掉数组最中间的元素。查找数组最中间的元素的代码应该适用于任何奇数长度的数组。
- 去掉数组的第一个值并显示它。
- 在数组前面添加
Rap
和Reggae
。
过程中的数组:
Jazz, Blues Jazz, Bues, Rock-n-Roll Jazz, Classics, Rock-n-Roll Classics, Rock-n-Roll Rap, Reggae, Classics, Rock-n-Roll。
let styles = ['Jazz', 'Blues']; styles.push('Rock-n-Roll'); styles[Math.floor((styles.length - 1) / 2)] = 'Classics'; console.log(styles.shift()); styles.unshift('Rap', 'Reggae');
- 创建一个数组
在数组上下文调用
结果是什么?为什么?
let arr = ["a", "b"]; arr.push(function() { alert( this ); }) arr[2](); // ?
数组对象。获取该函数的形式类似于对象的方法引用,然后调用方法。
输入数字求和
写出函数
sumInput()
,要求如下:- 使用
prompt
向用户索要值,并存在数组中。 - 当用户输入了非数字、空字符串或者点击“取消”按钮的时候,问询结束。
- 计算并返回数组所有项之和。
P.S.
0
是有效的数字,不要因为是 0 就停止问询。function sumInput() { let numbers = []; while (true) { let value = prompt("A number please?", 0); // 应该结束了吗? if (value === "" || value === null || !isFinite(value)) break; numbers.push(+value); } let sum = 0; for (let number of numbers) { sum += number; } return sum; } alert( sumInput() );
- 使用
最大子数组
输入是以数字组成的数组,例如
arr = [1, -2, 3, 4, -9, 6]
.任务是:找出所有项的和最大的
arr
数组的连续子数组。写出函数
getMaxSubSum(arr)
,用其找出并返回最大和。例如:
getMaxSubSum([-1, 2, 3, -9]) == 5(高亮项的加和) getMaxSubSum([2, -1, 2, 3, -9]) == 6 getMaxSubSum([-1, 2, 3, -9, 11]) == 11 getMaxSubSum([-2, -1, 1, 2]) == 3 getMaxSubSum([100, -9, 2, -3, 5]) == 100 getMaxSubSum([1, 2, 3]) == 6(所有项的和)
如果所有项都是负数,那就一个项也不取(子数组是空的),所以返回的是 0:
getMaxSubSum([-1, -2, -3]) = 0
请尝试想出一个快速的解决方案:复杂度可以是 O(n2),有能力达到 O(n) 则更好。
function getMaxSubSum(arr) { let len = arr.length; let result = 0; for (let i = 0; i < len; i++) { for (let j = i + 1; j <= len; j++) { let tmp = arr.slice(i, j); let sum = tmp.reduce((item, prev) => prev + item); if (sum > result) result = sum; } } return result; }
function getMaxSubSum(arr) { let result = 0; let tmp = 0; for (let item of arr) { tmp += item; if (tmp > result) result = tmp; if (tmp < 0) tmp = 0; } return result; }
数组方法
任务
将 border-left-width 转换成 borderLeftWidth
编写函数
camelize(str)
将诸如 “my-short-string” 之类的由短划线分隔的单词变成骆驼式的 “myShortString”。即:删除所有短横线,并将短横线后的每一个单词的首字母变为大写。
示例:
camelize("background-color") == 'backgroundColor'; camelize("list-style-image") == 'listStyleImage'; camelize("-webkit-transition") == 'WebkitTransition';
提示:使用
split
将字符串拆分成数组,对其进行转换之后再join
回来。function camelize(str) { let arr = str.split('-'); for (let i in arr) { if (i === '0') continue; arr[i] = arr[i][0].toUpperCase() + arr[i].slice(1); } return arr.join(''); }
过滤范围
写一个函数
filterRange(arr, a, b)
,该函数获取一个数组arr
,在其中查找数值大小在a
和b
之间的元素,并返回它们的数组。该函数不应该修改原数组。它应该返回新的数组。
例如:
let arr = [5, 3, 8, 1]; let filtered = filterRange(arr, 1, 4); alert( filtered ); // 3,1(匹配值) alert( arr ); // 5,3,8,1(未修改)
function filterRange(arr, a, b) { return arr.filter(item => item <= b && item >= a); }
原位 ( in place ) 过滤范围
写一个函数
filterRangeInPlace(arr, a, b)
,该函数获取一个数组arr
,并删除其中介于a
和b
区间以外的所有值。检查:a ≤ arr[i] ≤ b
。该函数应该只修改数组。它不应该返回任何东西。
例如:
let arr = [5, 3, 8, 1]; filterRangeInPlace(arr, 1, 4); // 删除了范围在 1 到 4 之外的所有值 alert( arr ); // [3, 1]
function filterRangeInPlace(arr, a, b) { arr.forEach((item, index) => { if (item > b || item < a) { arr.splice(index, 1); } }); }
降序排列
let arr = [5, 2, 1, -10, 8]; // ……你的代码以降序对其进行排序 alert( arr ); // 8, 5, 2, 1, -10
let arr = [5, 2, 1, -10, 8]; arr.sort((a, b) => b - a); console.log(arr); // 8, 5, 2, 1, -10
复制和排序数组
我们有一个字符串数组
arr
。我们希望有一个排序过的副本,但保持arr
不变。创建一个函数
copySorted(arr)
返回这样一个副本。let arr = ["HTML", "JavaScript", "CSS"]; let sorted = copySorted(arr); alert( sorted ); // CSS, HTML, JavaScript alert( arr ); // HTML, JavaScript, CSS (no changes)
function copySorted(arr) { return arr.map(item => item).sort(); }
创建一个可扩展的 calculator
创建一个构造函数
Calculator
,以创建“可扩展”的 calculator 对象。该任务由两部分组成。
首先,实现
calculate(str)
方法,该方法接受像"1 + 2"
这样格式为“数字 运算符 数字”(以空格分隔)的字符串,并返回结果。该方法需要能够理解加号+
和减号-
。用法示例:
let calc = new Calculator; alert( calc.calculate("3 + 7") ); // 10
然后添加方法
addMethod(name, func)
,该方法教 calculator 进行新操作。它需要运算符name
和实现它的双参数函数func(a,b)
。例如,我们添加乘法
*
,除法/
和求幂**
:let powerCalc = new Calculator; powerCalc.addMethod("*", (a, b) => a * b); powerCalc.addMethod("/", (a, b) => a / b); powerCalc.addMethod("**", (a, b) => a ** b); let result = powerCalc.calculate("2 ** 3"); alert( result ); // 8
- 此任务中没有括号或复杂的表达式。
- 数字和运算符之间只有一个空格。
- 你可以自行选择是否添加错误处理功能。
function Calculator() { this.method = { '+': (a, b) => a + b, '-': (a, b) => a - b }; this.calculate = function (str) { let arr = str.split(' '); let op = arr[1]; let result = this.method[op](+arr[0], +arr[2]); return result; }; this.addMethod = function (name, func) { this.method[name] = func; }; }
// 参考解决方案 function Calculator() { this.methods = { "-": (a, b) => a - b, "+": (a, b) => a + b }; this.calculate = function(str) { let split = str.split(' '), a = +split[0], op = split[1], b = +split[2] if (!this.methods[op] || isNaN(a) || isNaN(b)) { return NaN; } return this.methods[op](a, b); } this.addMethod = function(name, func) { this.methods[name] = func; }; }
映射到 names
你有一个
user
对象数组,每个对象都有user.name
。编写将其转换为 names 数组的代码。例如:
let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; let users = [ john, pete, mary ]; let names = /* ... your code */ alert( names ); // John, Pete, Mary
let names = users.map(item => item.name);
映射到对象
你有一个
user
对象数组,每个对象都有name
,surname
和id
。编写代码以该数组为基础,创建另一个具有
id
和fullName
的对象数组,其中fullName
由name
和surname
生成。例如:
let john = { name: "John", surname: "Smith", id: 1 }; let pete = { name: "Pete", surname: "Hunt", id: 2 }; let mary = { name: "Mary", surname: "Key", id: 3 }; let users = [ john, pete, mary ]; let usersMapped = /* ... your code ... */ /* usersMapped = [ { fullName: "John Smith", id: 1 }, { fullName: "Pete Hunt", id: 2 }, { fullName: "Mary Key", id: 3 } ] */ alert( usersMapped[0].id ) // 1 alert( usersMapped[0].fullName ) // John Smith
所以,实际上你需要将一个对象数组映射到另一个对象数组。在这儿尝试使用箭头函数
=>
来编写。let usersMapped = users.map(item => ({ id: item.id, fullName: item.name + ' ' + item.surname }));
按年龄对用户排序
编写函数
sortByAge(users)
获得对象数组的age
属性,并根据age
对这些对象数组进行排序。例如:
let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 28 }; let arr = [ pete, john, mary ]; sortByAge(arr); // now: [john, mary, pete] alert(arr[0].name); // John alert(arr[1].name); // Mary alert(arr[2].name); // Pete
function sortByAge(arr) { arr.sort((a, b) => a.age - b.age); }
随机排列数组
编写函数
shuffle(array)
来随机排列数组的元素。多次运行
shuffle
可能导致元素顺序的不同。例如:let arr = [1, 2, 3]; shuffle(arr); // arr = [3, 2, 1] shuffle(arr); // arr = [2, 1, 3] shuffle(arr); // arr = [3, 1, 2] // ...
所有元素顺序应该具有相等的概率。例如,可以将
[1,2,3]
重新排序为[1,2,3]
或[1,3,2]
或[3,1,2]
等,每种情况的概率相等。function shuffle(arr) { arr.sort((a, b) => (Math.random() > 0.5 ? 1 : -1)); }
获取平均年龄
编写
getAverageAge(users)
函数,该函数获取一个具有age
属性的对象数组,并返回平均年龄。平均值的计算公式是
(age1 + age2 + ... + ageN) / N
。例如:
let john = { name: "John", age: 25 }; let pete = { name: "Pete", age: 30 }; let mary = { name: "Mary", age: 29 }; let arr = [ john, pete, mary ]; alert( getAverageAge(arr) ); // (25 + 30 + 29) / 3 = 28
function getAverageAge(arr) { return arr.reduce((prev, item) => prev + item.age, 0) / arr.length; }
数组去重
arr
是一个数组。创建一个函数
unique(arr)
,返回去除重复元素后的数组arr
。例如:
function unique(arr) { /* your code */ } let strings = ["Hare", "Krishna", "Hare", "Krishna", "Krishna", "Krishna", "Hare", "Hare", ":-O" ]; alert( unique(strings) ); // Hare, Krishna, :-O
function unique(arr) { return Array.from(new Set(arr)); }
从数组创建键 ( 值 ) 对象
假设我们收到了一个用户数组,形式为:
{id:..., name:..., age... }
。创建一个函数
groupById(arr)
从该数组创建对象,以id
为键(key),数组项为值。例如:
let users = [ {id: 'john', name: "John Smith", age: 20}, {id: 'ann', name: "Ann Smith", age: 24}, {id: 'pete', name: "Pete Peterson", age: 31}, ]; let usersById = groupById(users); /* // 调用函数后,我们应该得到: usersById = { john: {id: 'john', name: "John Smith", age: 20}, ann: {id: 'ann', name: "Ann Smith", age: 24}, pete: {id: 'pete', name: "Pete Peterson", age: 31}, } */
处理服务端数据时,这个函数很有用。
在这个任务里我们假设
id
是唯一的。没有两个具有相同id
的数组项。请在解决方案中使用数组的
.reduce
方法。function groupById(users) { return users.reduce((obj, item) => { obj[item.id] = item; return obj; }, {}); }
类继承
任务
创建实例时出错
这里有一份
Rabbit
扩展Animal
的代码。不幸的是,
Rabbit
对象无法被创建。是哪里出错了呢?请解决它。class Animal { constructor(name) { this.name = name; } } class Rabbit extends Animal { constructor(name) { this.name = name; this.created = Date.now(); } } let rabbit = new Rabbit("White Rabbit"); // Error: this is not defined alert(rabbit.name);
没有调用
super
。扩展 clock
我们获得了一个
Clock
类。到目前为止,它每秒都会打印一次时间。class Clock { constructor({ template }) { this.template = template; } render() { let date = new Date(); let hours = date.getHours(); if (hours < 10) hours = '0' + hours; let mins = date.getMinutes(); if (mins < 10) mins = '0' + mins; let secs = date.getSeconds(); if (secs < 10) secs = '0' + secs; let output = this.template .replace('h', hours) .replace('m', mins) .replace('s', secs); console.log(output); } stop() { clearInterval(this.timer); } start() { this.render(); this.timer = setInterval(() => this.render(), 1000); } }
创建一个继承自
Clock
的新的类ExtendedClock
,并添加参数precision
— 每次 “ticks” 之间间隔的毫秒数,默认是1000
(1 秒)。- 你的代码应该在
extended-clock.js
文件里。 - 不要修改原有的
clock.js
。请扩展它。
class ExtendedClock extends Clock { constructor({ template, precision=1000 }) { super({ template }); this.precision = precision; } start() { this.render(); this.timer = setInterval(() => this.render(), this.precision); } }
- 你的代码应该在
Promise
任务
基于 promise 的延时
内建函数
setTimeout
使用了回调函数。请创建一个基于 promise 的替代方案。函数
delay(ms)
应该返回一个 promise。这个 promise 应该在ms
毫秒后被 resolve,所以我们可以向其中添加.then
,像这样:function delay(ms) { // 你的代码 } delay(3000).then(() => alert('runs after 3 seconds'));
function delay(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms); }); }
Async/await
任务
用 async/await 来重写
重写下面这个来自 Promise 链 一章的示例代码,使用
async/await
而不是.then/catch
:function loadJson(url) { return fetch(url) .then(response => { if (response.status == 200) { return response.json(); } else { throw new Error(response.status); } }); } loadJson('no-such-user.json') .catch(alert); // Error: 404
async function loadJson(url) { let response = await fetch(url); if (response.status == 200) { let json = await response.json(); return json; } return throw new Error(response.status); } loadJson('no-such-user.json').catch(alert); // Error: 404
使用 async/await 重写 “rethrow”
下面你可以看到 “rethrow” 的例子。让我们来用
async/await
重写它,而不是使用.then/catch
。同时,我们可以在
demoGithubUser
中使用循环以摆脱递归:在async/await
的帮助下很容易实现。class HttpError extends Error { constructor(response) { super(`${response.status} for ${response.url}`); this.name = 'HttpError'; this.response = response; } } function loadJson(url) { return fetch(url) .then(response => { if (response.status == 200) { return response.json(); } else { throw new HttpError(response); } }); } // 询问用户名,直到 github 返回一个合法的用户 function demoGithubUser() { let name = prompt("Enter a name?", "iliakan"); return loadJson(`https://api.github.com/users/${name}`) .then(user => { alert(`Full name: ${user.name}.`); return user; }) .catch(err => { if (err instanceof HttpError && err.response.status == 404) { alert("No such user, please reenter."); return demoGithubUser(); } else { throw err; } }); } demoGithubUser();
// ... async function loadJson(url) { let response = await fetch(url); if (response.status == 200) { let json = await response.json(); return json; } else { throw new HttpError(response); } } // 询问用户名,直到 github 返回一个合法的用户 async function demoGithubUser() { let user; while (true) { let name = prompt('Enter a name?', 'iliakan'); try { user = await loadJson(`https://api.github.com/users/${name}`); break; } catch (err) { if (err instanceof HttpError && err.response.status == 404) { alert('No such user, please reenter.'); } else { throw err; } } } } // ...
在非 async 函数中调用 async 函数
我们有一个名为
f
的“普通”函数。你会怎样调用async
函数wait()
并在f
中使用其结果?async function wait() { await new Promise(resolve => setTimeout(resolve, 1000)); return 10; } function f() { // ……这里你应该怎么写? // 我们需要调用 async wait() 并等待以拿到结果 10 // 记住,我们不能使用 "await" }
P.S. 这个任务其实很简单,但是对于 async/await 新手开发者来说,这个问题却很常见。
function f() { wait().then(res => console.log(res)); }
Generator
任务
伪随机 generator
在很多地方我们都需要随机数据。
其中之一就是测试。我们可能需要随机数据:文本,数字等,以便很好地进行测试。
在 JavaScript 中,我们可以使用
Math.random()
。但是如果什么地方出现了问题,我们希望能使用完全相同的数据进行重复测试。为此,我们可以使用所谓的“种子伪随机(seeded pseudo-random)generator”。它们将“种子(seed)”作为第一个值,然后使用公式生成下一个值。以便相同的种子(seed)可以产出(yield)相同的序列,因此整个数据流很容易复现。我们只需要记住种子并重复它即可。
这样的公式的一个示例如下,它可以生成一些均匀分布的值:
next = previous * 16807 % 2147483647
如果我们使用
1
作为种子,生成的值将会是:16807
282475249
1622650073
- ……等……
这里的任务是创建一个 generator 函数
pseudoRandom(seed)
,它将seed
作为参数并使用此公式创建 generator。使用范例:
let generator = pseudoRandom(1); alert(generator.next().value); // 16807 alert(generator.next().value); // 282475249 alert(generator.next().value); // 1622650073
function* pseudoRandom(seed) { let previous = seed; while (true) { previous = (previous * 16807) % 2147483647; yield previous; } }
Proxy 和 Reflect
任务
读取不存在的属性时出错
通常,尝试读取不存在的属性会返回
undefined
。创建一个代理,在尝试读取不存在的属性时,该代理抛出一个错误。
这可以帮助及早发现编程错误。
编写一个函数
wrap(target)
,该函数接受一个target
对象,并返回添加此方面功能的代理(proxy)。其工作方式应如下:
let user = { name: "John" }; function wrap(target) { return new Proxy(target, { /* 你的代码 */ }); } user = wrap(user); alert(user.name); // John alert(user.age); // ReferenceError: Property doesn't exist: "age"
function wrap(target) { return new Proxy(target, { get(target, prop, receiver) { if (prop in target) { return Reflect.get(...arguments); } else { throw new ReferenceError(`Property doesn't exist: "${prop}"`); } } }); }
访问 array[-1]
在某些编程语言中,我们可以使用从尾端算起的负值索引访问数组元素。
像这样:
let array = [1, 2, 3]; array[-1]; // 3,最后一个元素 array[-2]; // 2,从尾端开始向前移动一步 array[-3]; // 1,从尾端开始向前移动两步
换句话说,
array[-N]
与array[array.length - N]
相同。创建一个 proxy 来实现该行为。
其工作方式应如下:
let array = [1, 2, 3]; array = new Proxy(array, { /* 你的代码 */ }); alert( array[-1] ); // 3 alert( array[-2] ); // 2 // 其他数组功能应保持“原样”
array = new Proxy(array, { get(target, prop, receiver) { if (prop < 0) { prop = +target.length + +prop; } return Reflect.get(target, prop, receiver); } });
可观察的 (Observable)
创建一个函数
makeObservable(target)
,该函数通过返回一个代理“使得对象可观察”。其工作方式如下:
function makeObservable(target) { /* 你的代码 */ } let user = {}; user = makeObservable(user); user.observe((key, value) => { alert(`SET ${key}=${value}`); }); user.name = "John"; // alerts: SET name=John
换句话说,
makeObservable
返回的对象就像原始对象一样,但是具有observe(handler)
方法,该方法可以将handler
函数设置为在任何属性被更改时,都会被调用的函数。每当有属性被更改时,都会使用属性的名称和属性值调用
handler(key, value)
函数。P.S. 在本任务中,你可以只关注属性写入。其他的操作可以通过类似的方式实现。
function makeObservable(target) { const cbList = []; let res = new Proxy(target, { set(target, prop, value, receiver) { cbList.forEach(cb => cb(prop, value)); return Reflect.set(target, prop, value, receiver); } }); res.observe = function (cb) { cbList.push(cb); }; return res; } // 参考解法 let handlers = Symbol('handlers'); function makeObservable(target) { // 1. 初始化 handler 存储 target[handlers] = []; // 将 handler 函数存储到数组中,以便于之后调用 target.observe = function(handler) { this[handlers].push(handler); }; // 2. 创建一个 proxy 以处理更改 return new Proxy(target, { set(target, property, value, receiver) { let success = Reflect.set(...arguments); // 将操作转发给对象 if (success) { // 如果在设置属性时没有出现 error // 调用所有 handler target[handlers].forEach(handler => handler(property, value)); } return success; } }); }
节点属性:type,tag 和 content
任务
计数后代
这里有一个树结构嵌套的
ul/li
。编写代码,为每个
<li>
显示:- 里面的文本内容是什么(没有子树)
- 嵌套的
<li>
的数量 — 所有后代,包括深层嵌套的后代。
// 本人稀烂的解法 function isLi(el) { const tagName = el.tagName; if (tagName && tagName.toLowerCase() === 'li') return true; return false; } function countNumOfLi(el) { if (!el) return 0; let children = el.children; let count = 0; for (let i = 0; i < children.length; i++) { let item = children[i]; if (isLi(item)) { count++; } count += countNumOfLi(item); } return count; } function handler(list) { for (let i = 0; i < list.length; i++) { let item = list[i]; let count = countNumOfLi(item); // console.dir(item); if (isLi(item)) { console.log(item.childNodes[0].data.trim() + count); } if (item.children) { handler(item.children); } } } let ul = document.querySelector('ul'); let lis = ul.children; handler(lis);
for (let li of document.querySelectorAll('li')) { // get the title from the text node let title = li.firstChild.data; title = title.trim(); // remove extra spaces from ends // get the descendants count let count = li.getElementsByTagName('li').length; alert(title + ': ' + count); }
特性和属性(Attributes and properties)
任务
将外部链接设为橙色
通过修改
style
属性,将所有外部链接变为橙色。如果一个链接是外部的:
- 其
href
中包含://
- 但不是以
http://internal.com
开头。
例如:
<a name="list">the list</a> <ul> <li><a href="http://google.com">http://google.com</a></li> <li><a href="/tutorial">/tutorial.html</a></li> <li><a href="local/path">local/path</a></li> <li><a href="ftp://ftp.com/my.zip">ftp://ftp.com/my.zip</a></li> <li><a href="http://nodejs.org">http://nodejs.org</a></li> <li><a href="http://internal.com/test">http://internal.com/test</a></li> </ul> <script> // 为单个链接设置样式 let link = document.querySelector('a'); link.style.color = 'orange'; </script>
结果应该是:
// 1 let links = document.querySelectorAll('a'); for (let link of links) { let href = link.getAttribute('href'); if (!href) continue; // 没有特性 if (!href.includes('://')) continue; // 没有协议 if (href.startsWith('http://internal.com')) continue; // 内部的 link.style.color = 'orange'; } // 2 // 查找所有 href 中包含 :// 的链接 // 但 href 不是以 http://internal.com 开头 let selector = 'a[href*="://"]:not([href^="http://internal.com"])'; let links = document.querySelectorAll(selector); links.forEach(link => link.style.color = 'orange');
- 其
修改文档 (document)
任务
从对象创建树
编写一个函数
createTree
,从嵌套对象创建一个嵌套的ul/li
列表(list)。例如:
let data = { "Fish": { "trout": {}, "salmon": {} }, "Tree": { "Huge": { "sequoia": {}, "oak": {} }, "Flowering": { "apple tree": {}, "magnolia": {} } } };
语法:
let container = document.getElementById('container'); createTree(container, data); // 将树创建在 container 中
结果(树)看起来像这样:
选择下面两种方式中的一种,来完成这个任务:
- 为树创建 HTML,然后将它们赋值给
container.innerHTML
。 - 创建节点树,并使用 DOM 方法将它们附加(append)上去。
如果这两种方式你都做,那就太好了。
P.S. 树上不应该有“多余”的元素,例如空的
<ul></ul>
叶子节点。function createTree(container, data) { const ul = document.createElement('ul'); for (let key in data) { const value = data[key]; const li = document.createElement('li'); li.append(key); createTree(li, value); ul.append(li); } container.append(ul); return ul; }
参考解法:
// innerHTML function createTree(container, obj) { container.innerHTML = createTreeText(obj); } function createTreeText(obj) { // standalone recursive function let li = ''; let ul; for (let key in obj) { li += '<li>' + key + createTreeText(obj[key]) + '</li>'; } if (li) { ul = '<ul>' + li + '</ul>' } return ul || ''; } // DOM function createTree(container, obj) { container.append(createTreeDom(obj)); } function createTreeDom(obj) { // if there's no children, then the call returns undefined // and the <ul> won't be created if (!Object.keys(obj).length) return; let ul = document.createElement('ul'); for (let key in obj) { let li = document.createElement('li'); li.innerHTML = key; let childrenUl = createTreeDom(obj[key]); if (childrenUl) { li.append(childrenUl); } ul.append(li); } return ul; }
- 为树创建 HTML,然后将它们赋值给
在树中显示后代
这里有一棵由嵌套的
ul/li
组成的树。编写代码,为每个
<li>
添加其后代数量。跳过叶子节点(没有子代的节点)。结果:
const lis = document.querySelectorAll('li'); for (let li of lis) { const childLen = li.getElementsByTagName('li').length; if (childLen === 0) continue; const first = li.firstChild; const text = first.textContent.trim() + `[${childLen}]`; first.data = text; }
参考解法:
let lis = document.getElementsByTagName('li'); for (let li of lis) { // get the count of all <li> below this <li> let descendantsCount = li.getElementsByTagName('li').length; if (!descendantsCount) continue; // add directly to the text node (append to the text) li.firstChild.data += ' [' + descendantsCount + ']'; }
创建一个日历
编写一个函数
createCalendar(elem, year, month)
。对该函数的调用,应该使用给定的 year/month 创建一个日历,并将创建的日历放入
elem
中。创建的日历应该是一个表格(table),其中每一周用
<tr>
表示,每一天用<td>
表示。表格顶部应该是带有星期名的<th>
:第一天应该是 Monday,依此类推,直到 Sunday。例如,
createCalendar(cal, 2012, 9)
应该在元素cal
中生成如下所示的日历:P.S. 在这个任务中,生成一个日历就可以了,不需要有点击交互的功能。
参考解法:
function createCalendar(elem, year, month) { let mon = month - 1; // months in JS are 0..11, not 1..12 let d = new Date(year, mon); let table = '<table><tr><th>MO</th><th>TU</th><th>WE</th><th>TH</th><th>FR</th><th>SA</th><th>SU</th></tr><tr>'; // spaces for the first row // from Monday till the first day of the month // * * * 1 2 3 4 for (let i = 0; i < getDay(d); i++) { table += '<td></td>'; } // <td> with actual dates while (d.getMonth() == mon) { table += '<td>' + d.getDate() + '</td>'; if (getDay(d) % 7 == 6) { // sunday, last day of week - newline table += '</tr><tr>'; } d.setDate(d.getDate() + 1); } // add spaces after last days of month for the last row // 29 30 31 * * * * if (getDay(d) != 0) { for (let i = getDay(d); i < 7; i++) { table += '<td></td>'; } } // close the table table += '</tr></table>'; elem.innerHTML = table; } function getDay(date) { // get day number from 0 (monday) to 6 (sunday) let day = date.getDay(); if (day == 0) day = 7; // make Sunday (0) the last day return day - 1; }
使用 setInterval 的彩色时钟
创建一个像这样的彩色时钟:
使用 HTML/CSS 进行样式设计,JavaScript 仅用来更新元素中的时间。
<div id="clock"> <span class="hour">hh</span>:<span class="min">mm</span>:<span class="sec">ss</span> </div>
function update() {
let clock = document.getElementById('clock');
let date = new Date(); // (*)
let hours = date.getHours();
if (hours < 10) hours = '0' + hours;
clock.children[0].innerHTML = hours;
let minutes = date.getMinutes();
if (minutes < 10) minutes = '0' + minutes;
clock.children[1].innerHTML = minutes;
let seconds = date.getSeconds();
if (seconds < 10) seconds = '0' + seconds;
clock.children[2].innerHTML = seconds;
}
let timerId;
function clockStart() { // 运行时钟
timerId = setInterval(update, 1000);
update(); // (*)
}
function clockStop() {
clearInterval(timerId);
timerId = null;
}
对表格进行排序
下面是一个表格:
<table> <thead> <tr> <th>Name</th><th>Surname</th><th>Age</th> </tr> </thead> <tbody> <tr> <td>John</td><td>Smith</td><td>10</td> </tr> <tr> <td>Pete</td><td>Brown</td><td>15</td> </tr> <tr> <td>Ann</td><td>Lee</td><td>5</td> </tr> <tr> <td>...</td><td>...</td><td>...</td> </tr> </tbody> </table>
可能会有更多行。
编写代码,按
"name"
列对其进行排序。let sortedRows = Array.from(table.tBodies[0].rows) // 1 .sort((rowA, rowB) => rowA.cells[0].innerHTML.localeCompare(rowB.cells[0].innerHTML)); table.tBodies[0].append(...sortedRows); // (3)
样式和类
任务
创建一个通知
编写一个函数
showNotification(options)
:该函数创建一个带有给定内容的通知<div class="notification">
。该通知应该在 1.5 秒后自动消失。参数:
// 在窗口的右上角附近显示一个带有文本 "Hello" 的元素 showNotification({ top: 10, // 距窗口顶部 10px(默认为 0px) right: 10, // 距窗口右边缘 10px(默认为 0px) html: "Hello!", // 通知中的 HTML className: "welcome" // div 的附加类(可选) });
使用 CSS 定位在给定的 top/right 坐标处显示元素。源文档已经提供了必要的样式。
function showNotification({ top = 0, right = 0, className, html }) { const toast = document.createElement('div'); toast.style.top = top + 'px'; toast.style.right = right + 'px'; toast.innerHTML = html; toast.classList.add('notification'); toast.classList.add(className); document.body.append(toast); setTimeout(() => { toast.remove(); }, 1500); }
参考解法:
function showNotification({top = 0, right = 0, className, html}) { let notification = document.createElement('div'); notification.className = "notification"; if (className) { notification.classList.add(className); } notification.style.top = top + 'px'; notification.style.right = right + 'px'; notification.innerHTML = html; document.body.append(notification); setTimeout(() => notification.remove(), 1500); }
元素大小和滚动
任务
将小球置于区域(filed)中心
源文件的效果如下:
区域(field)的中心坐标是多少?
计算它们,并将小球置于绿色的区域(field)中心:
- 该元素应该通过 JavaScript 移动,而不是 CSS。
- 该代码应该适用于任何大小的球(
10
、20
、30
像素)以及任意大小的区域(field),而不应该绑定到给定值。
P.S. 当然了,置于中心的操作通过 CSS 也可以完成,但是这里我们需要通过 JavaScript 完成。此外,当必须使用 JavaScript 时,我们可能会遇到其他话题以及更加复杂的情况,这里我们只是做一个“热身”。
const field = document.querySelector('#field'); const ball = document.querySelector('#ball'); ball.onload = function () { const fieldWidth = field.clientWidth; const fieldHeight = field.clientHeight; const ballWidth = ball.offsetWidth; console.log(fieldWidth, fieldHeight, ballWidth); ball.style.left = fieldWidth / 2 - ballWidth / 2 + 'px'; ball.style.top = fieldHeight / 2 - ballWidth / 2 + 'px'; };
此处调用
ball.onload
原因为ball
元素为图片,在未加载完成前无法取得宽高,需要在图片加载完成后再进行读取计算。或者给img
显式设置宽高。
坐标
任务
查找区域的窗口坐标
在下面的 iframe 中,你可以看到一个带有绿色区域(field)的文档。
使用 JavaScript 查找带箭头指向的角的窗口坐标。
为了方便起见,已经在文档中实现了一个小功能。在任何地方点击都会显示那里的坐标。
你的代码应该使用 DOM 来获取以下窗口坐标:
- 左上的外角(这很简单)。
- 右下的外角(也挺简单)。
- 左上的内角(这有点难)。
- 右下的内角(有几种方式,选择其中一种)。
你计算得到的坐标,应该与点击鼠标返回的坐标相同。
P.S. 如果元素具有其他大小(size)和边框(border),且未绑定任何固定的值,你写的代码也应该起作用。
let coords = elem.getBoundingClientRect(); let answer1 = [coords.left, coords.top]; let answer2 = [coords.right, coords.bottom]; let answer3 = [coords.left + field.clientLeft, coords.top + field.clientTop]; let answer4 = [ coords.right - parseInt(getComputedStyle(field).borderRightWidth), coords.bottom - parseInt(getComputedStyle(field).borderBottomWidth) ]; // or let answer4 = [ coords.left + elem.clientLeft + elem.clientWidth, coords.top + elem.clientTop + elem.clientHeight ];
在元素旁显示一个 note
创建一个函数
positionAt(anchor, position, elem)
来定位elem
,具体取决于anchor
元素附近的position
。position
必须具有下列三个字符串中的一个:"top"
— 将elem
定位在anchor
上方"right"
— 将elem
定位在anchor
右侧"bottom"
— 将elem
定位在anchor
下方
position
被用在函数showNote(anchor, position, html)
内,该函数使用给定的html
创建一个 “note” 元素,并将其显示在anchor
附近的position
处。这是一个演示示例:
function positionAt(anchor, position, elem) { let pos = { x: 0, y: 0 }; const data = anchor.getBoundingClientRect(); const self = elem.getBoundingClientRect(); switch (position) { case 'top': pos.x = data.x; pos.y = data.y - self.height; break; case 'right': pos.x = data.right; pos.y = data.y; break; case 'bottom': pos.x = data.x; pos.y = data.bottom; break; default: break; } elem.style.left = pos.x + 'px'; elem.style.top = pos.y + 'px'; } // 参考解法 function positionAt(anchor, position, elem) { let anchorCoords = anchor.getBoundingClientRect(); switch (position) { case "top": elem.style.left = anchorCoords.left + "px"; elem.style.top = anchorCoords.top - elem.offsetHeight + "px"; break; case "right": elem.style.left = anchorCoords.left + anchor.offsetWidth + "px"; elem.style.top = anchorCoords.top + "px"; break; case "bottom": elem.style.left = anchorCoords.left + "px"; elem.style.top = anchorCoords.top + anchor.offsetHeight + "px"; break; } }
在元素旁 (absolute) 显示一个 note
修改 上一个任务 的解决方案,让 note 元素使用
position:absolute
来替代position:fixed
。这可以防止页面滚动时元素的“失控”。
以上一个任务的解决方案为基础。为了测试页面滚动,请添加样式
<body style="height: 2000px">
。function getCoords(elem) { let box = elem.getBoundingClientRect(); return { top: box.top + window.pageYOffset, left: box.left + window.pageXOffset }; } function positionAt(anchor, position, elem) { let anchorCoords = getCoords(anchor); switch (position) { case "top": elem.style.left = anchorCoords.left + "px"; elem.style.top = anchorCoords.top - elem.offsetHeight + "px"; break; case "right": elem.style.left = anchorCoords.left + anchor.offsetWidth + "px"; elem.style.top = anchorCoords.top + "px"; break; case "bottom": elem.style.left = anchorCoords.left + "px"; elem.style.top = anchorCoords.top + anchor.offsetHeight + "px"; break; } }
把 note 放在元素内部 (absolute)
扩展上一个任务 在元素旁(absolute)显示一个 note:教函数
positionAt(anchor, position, elem)
把elem
插入到anchor
内部。position
的新值:top-out
,right-out
,bottom-out
— 和之前一样工作,它们把elem
插入到anchor
的上方/右侧/下方。top-in
,right-in
,bottom-in
— 把elem
插入到anchor
内部:将其粘贴到上/右/下边缘。
例如:
// 在 blockquote 上方显示 note positionAt(blockquote, "top-out", note); // 在 blockquote 内部的上边缘显示 note positionAt(blockquote, "top-in", note);
结果:
可以以上一个任务 在元素旁(absolute)显示一个 note 的解决方案为基础。
function getCoords(elem) { let box = elem.getBoundingClientRect(); return { top: box.top + window.pageYOffset, left: box.left + window.pageXOffset }; } function positionAt(anchor, position, elem) { let anchorCoords = getCoords(anchor); switch (position) { case 'top-in': elem.style.left = anchorCoords.left + 'px'; elem.style.top = anchorCoords.top + 'px'; break; case 'top-out': elem.style.left = anchorCoords.left + 'px'; elem.style.top = anchorCoords.top - elem.offsetHeight + 'px'; break; case 'right-in': elem.style.left = anchorCoords.left + anchor.offsetWidth - elem.offsetWidth + 'px'; elem.style.top = anchorCoords.top + 'px'; break; case 'right-out': elem.style.left = anchorCoords.left + anchor.offsetWidth + 'px'; elem.style.top = anchorCoords.top + 'px'; break; case 'bottom-in': elem.style.left = anchorCoords.left + 'px'; elem.style.top = anchorCoords.top + anchor.offsetHeight - elem.offsetHeight + 'px'; break; case 'bottom-out': elem.style.left = anchorCoords.left + 'px'; elem.style.top = anchorCoords.top + anchor.offsetHeight + 'px'; break; } }
浏览器事件简介
任务
点击隐藏
为
button
添加 JavaScript 代码,使得<div id="text">
在我们点击该按钮时消失。示例:
hider.onclick = function () { text.remove() } // 参考解法 document.getElementById('hider').onclick = function() { document.getElementById('text').hidden = true; }
隐藏自己
创建一个按钮,在被点击时,隐藏自己。
hider.onclick = function () { this.hidden = true; }
哪个处理程序会运行?
在变量中有一个按钮。它上面没有处理程序。
执行以下代码之后,哪些处理程序会在按钮被点击时运行?会显示哪些 alert?
button.addEventListener("click", () => alert("1")); button.removeEventListener("click", () => alert("1")); button.onclick = () => alert(2);
第 1 行和第 5 行的处理程序会运行。
让球在球场中移动
点击球场中任意一点,让球在球场中移动。就像这样:
要求:
- 球的中心应该恰好在点击时鼠标指针位置的下方(如果在球不越过球场边缘的情况下,能实现的话)。
- 最好添加一些 CSS 动画。
- 球不能越过场地边界。
- 页面滚动时,不会有任何中断。
注意:
- 代码还应该适用于不同大小的球和球场,而不应该绑定到任何固定值。
- 使用
event.clientX/event.clientY
属性来获取点击坐标。
#ball { left: 0; top: 0; position: relative; transition: all 0.5s; }
const field = document.querySelector('#field'); const ball = document.querySelector('#ball'); field.addEventListener('click', e => { const x = e.offsetX; const y = e.offsetY; const width = ball.width; ball.style.left = x - width / 2 + 'px'; ball.style.top = y - width / 2 + 'px'; });
// 参考解法 #field { width: 200px; height: 150px; border: 10px solid black; background-color: #00FF00; position: relative; overflow: hidden; cursor: pointer; } #ball { position: absolute; left: 0; top: 0; width: 40px; height: 40px; transition: all 1s; }
field.onclick = function(event) { // window-relative field coordinates let fieldCoords = this.getBoundingClientRect(); // the ball has position:absolute, the field: position:relative // so ball coordinates are relative to the field inner left-upper corner let ballCoords = { top: event.clientY - fieldCoords.top - field.clientTop - ball.clientHeight / 2, left: event.clientX - fieldCoords.left - field.clientLeft - ball.clientWidth / 2 }; // prevent crossing the top field boundary if (ballCoords.top < 0) ballCoords.top = 0; // prevent crossing the left field boundary if (ballCoords.left < 0) ballCoords.left = 0;
// // prevent crossing the right field boundary
if (ballCoords.left + ball.clientWidth > field.clientWidth) {
ballCoords.left = field.clientWidth - ball.clientWidth;
}
// prevent crossing the bottom field boundary
if (ballCoords.top + ball.clientHeight > field.clientHeight) {
ballCoords.top = field.clientHeight - ball.clientHeight;
}
ball.style.left = ballCoords.left + 'px';
ball.style.top = ballCoords.top + 'px';
}
5. 创建滑动菜单
创建一个在点击时打开/折叠的菜单:
<iframe class="code-result__iframe" data-trusted="1" src="https://zh.js.cx/task/sliding-menu/solution/" style="display: block; border: 0px; width: 798px; height: 100px;"></iframe>
P.S. 源文档的 HTML/CSS 将被修改。
```html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
.hidden {
display: none;
}
.arrow {
color: green;
}
</style>
</head>
<body>
<div id="toggler">
<span id="open" class="arrow">▶</span
><span id="close" class="arrow hidden">▼</span> Sweeties (click me)!
</div>
<ul id="content" class="hidden">
<li>Cake</li>
<li>Donut</li>
<li>Honey</li>
</ul>
<script>
const toggler = document.querySelector('#toggler');
const content = document.querySelector('#content');
const open = document.querySelector('#open');
const close = document.querySelector('#close');
let isOpen = false;
toggler.onclick = function () {
close.classList.toggle('hidden');
open.classList.toggle('hidden');
content.classList.toggle('hidden');
isOpen = !isOpen;
};
</script>
</body>
</html>
参考解法:
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<style>
.menu ul {
margin: 0;
list-style: none;
padding-left: 20px;
display: none;
}
.menu .title {
font-size: 18px;
cursor: pointer;
}
.menu .title::before {
content: '▶ ';
font-size: 80%;
color: green;
}
.menu.open .title::before {
content: '▼ ';
}
.menu.open ul {
display: block;
}
</style>
</head>
<body>
<div id="sweeties" class="menu">
<span class="title">Sweeties (click me)!</span>
<ul>
<li>Cake</li>
<li>Donut</li>
<li>Honey</li>
</ul>
</div>
<script>
let menuElem = document.getElementById('sweeties');
let titleElem = menuElem.querySelector('.title');
titleElem.onclick = function() {
menuElem.classList.toggle('open');
};
</script>
</body>
</html>
注意点:如果 click me
处设置为 div
会导致整行都可以触发点击事件。
添加关闭按钮
有一个消息列表。
使用 JavaScript 在每条消息的右上角添加一个关闭按钮。
结果应该如下所示:
.pane { background: #edf5e1; padding: 10px 20px 10px; border-top: solid 2px #c4df9b; + position: relative; } .pane .remove-button { position: absolute; right: 10px; top: 0; }
const panes = document.querySelectorAll('.pane'); panes.forEach(pane => { const button = createButton(); button.onclick = function () { pane.remove(); }; pane.append(button); }); function createButton() { const button = document.createElement('button'); button.classList.add('remove-button'); button.textContent = '[x]'; return button; }
参考解法:
.pane { background: #edf5e1; padding: 10px 20px 10px; border-top: solid 2px #c4df9b; position: relative; } .remove-button { position: absolute; font-size: 110%; top: 0; color: darkred; right: 10px; display: block; width: 24px; height: 24px; border: none; background: transparent; cursor: pointer; }
let panes = document.querySelectorAll('.pane'); for(let pane of panes) { pane.insertAdjacentHTML("afterbegin", '<button class="remove-button">[x]</button>'); // button becomes the first child of pane pane.firstChild.onclick = () => pane.remove(); }
轮播图
创建一个“轮播图(carousel)” —— 一条可以通过点击箭头来滚动图像的图像带。
之后,我们可以为其添加更多功能:无限滚动,动态加载等。
P.S. 对于这个任务,HTML/CSS 结构实际上占解决方案的 90%。
.arrow { padding: 0; background: #ddd; border-radius: 15px; border: 1px solid gray; font-size: 24px; line-height: 24px; color: #444; display: block; position: absolute; top: 50%; transform: translateY(-50%); } .arrow.left { left: 10px; } .arrow.right { right: 10px; } .arrow:focus { outline: none; } .arrow:hover { background: #ccc; cursor: pointer; } #carousel { height: 150px; width: 450px; padding: 10px 30px; background-color: #eee; border: 1px solid #ccc; border-radius: 20px; overflow: hidden; position: relative; box-sizing: border-box; } ul { height: 130px; width: 9999px; margin: 0; padding: 0; list-style: none; font-size: 0; transition: transform 0.3s; } ul img { width: 130px; height: 130px; display: block; /* removes extra space near images */ } ul li { display: inline-block; /* removes extra space between list items */ }
<!DOCTYPE html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="index.css"> </head> <body> <div id="carousel"> <ul style='transform: translateX(0px);'> <li><img src="https://en.js.cx/carousel/1.png"></li> <li><img src="https://en.js.cx/carousel/2.png"></li> <li><img src="https://en.js.cx/carousel/3.png"></li> <li><img src="https://en.js.cx/carousel/4.png"></li> <li><img src="https://en.js.cx/carousel/5.png"></li> <li><img src="https://en.js.cx/carousel/6.png"></li> <li><img src="https://en.js.cx/carousel/7.png"></li> <li><img src="https://en.js.cx/carousel/8.png"></li> <li><img src="https://en.js.cx/carousel/9.png"></li> <li><img src="https://en.js.cx/carousel/10.png"></li> </ul> <button class="arrow left">⇦</button> <button class="arrow right">⇨</button> </div> <script> // label the images to visually track them, just for convenience, // this code can be removed let i = 1; for(let li of carousel.querySelectorAll('li')) { li.style.position = 'relative'; li.insertAdjacentHTML('beforeend', `<span style="position:absolute;left:0;top:0">${i}</span>`); i++; } let index = 0; const arrowLeft = document.querySelector('.arrow.left'); const arrowRight = document.querySelector('.arrow.right'); const ul = document.querySelector('#carousel ul'); arrowRight.onclick = function () { index += 3; if (index > 6) { index = 7; } move(index) } arrowLeft.onclick = function () { index-=3; if (index < 0) { index = 0; } move(index); } function move(index) { const pos = index * -130; const text = `translateX(${pos}px)` ul.style.transform = text; } </script> </body> </html>
参考解法:
body { padding: 10px; } .carousel { position: relative; width: 398px; padding: 10px 40px; border: 1px solid #CCC; border-radius: 15px; background: #eee; } .carousel img { width: 130px; height: 130px; /* make it block to remove space around images */ display: block; } .arrow { position: absolute; top: 60px; padding: 0; background: #ddd; border-radius: 15px; border: 1px solid gray; font-size: 24px; line-height: 24px; color: #444; display: block; } .arrow:focus { outline: none; } .arrow:hover { background: #ccc; cursor: pointer; } .prev { left: 7px; } .next { right: 7px; } .gallery { width: 390px; overflow: hidden; } .gallery ul { height: 130px; width: 9999px; margin: 0; padding: 0; list-style: none; transition: margin-left 250ms; /* remove white-space between inline-block'ed li */ /* http://davidwalsh.name/remove-whitespace-inline-block */ font-size: 0; } .gallery li { display: inline-block; }
<!DOCTYPE html> <head> <meta charset="utf-8"> <link rel="stylesheet" href="style.css"> </head> <body> <div id="carousel" class="carousel"> <button class="arrow prev">⇦</button> <div class="gallery"> <ul class="images"> <li><img src="https://en.js.cx/carousel/1.png"></li> <li><img src="https://en.js.cx/carousel/2.png"></li> <li><img src="https://en.js.cx/carousel/3.png"></li> <li><img src="https://en.js.cx/carousel/4.png"></li> <li><img src="https://en.js.cx/carousel/5.png"></li> <li><img src="https://en.js.cx/carousel/6.png"></li> <li><img src="https://en.js.cx/carousel/7.png"></li> <li><img src="https://en.js.cx/carousel/8.png"></li> <li><img src="https://en.js.cx/carousel/9.png"></li> <li><img src="https://en.js.cx/carousel/10.png"></li> </ul> </div> <button class="arrow next">⇨</button> </div> <script> /* label the images, just for convenience, to visually track them */ let i = 1; for(let li of carousel.querySelectorAll('li')) { li.style.position = 'relative'; li.insertAdjacentHTML('beforeend', `<span style="position:absolute;left:0;top:0">${i}</span>`); i++; } /* configuration */ let width = 130; // image width let count = 3; // visible images count let list = carousel.querySelector('ul'); let listElems = carousel.querySelectorAll('li'); let position = 0; // ribbon scroll position carousel.querySelector('.prev').onclick = function() { // shift left position += width * count; // can't move to the left too much, end of images position = Math.min(position, 0) list.style.marginLeft = position + 'px'; }; carousel.querySelector('.next').onclick = function() { // shift right position -= width * count; // can only shift the ribbbon for (total ribbon length - visible count) images position = Math.max(position, -width * (listElems.length - count)); list.style.marginLeft = position + 'px'; }; </script> </body> </html>
事件委托
任务
使用委托隐藏消息
有一个带有移除按钮
[x]
的消息列表。让按钮可以工作。就像这样:
P.S. 在容器上应该只有一个事件监听器,请使用事件委托。
document.addEventListener('click', event => { let classList = event.target.classList; if (classList && classList[0] === 'remove-button') { let elem = event.target.closest('div'); elem.remove(); } }); // 参考解法 container.onclick = function(event) { if (event.target.className != 'remove-button') return; let pane = event.target.closest('.pane'); pane.remove(); };
树形菜单
创建一个点击可以显示/隐藏子节点的树形菜单:
要求:
- 只能有一个事件处理程序(使用委托)。
- 对节点标题以外(在空白处)的点击不会做任何处理。
.hidden { display: none; }
document.addEventListener('click', event => { let target = event.target; if (target.tagName.toLowerCase() === 'li') { let elem = target.children; if (elem.length && elem[0].tagName.toLowerCase() === 'ul') { elem[0].classList.toggle('hidden'); } } });
参考解法:
.tree span:hover { font-weight: bold; } .tree span { cursor: pointer; }
// move all text into <span> // they occupy exactly the place necessary for the text, for (let li of tree.querySelectorAll('li')) { let span = document.createElement('span'); li.prepend(span); span.append(span.nextSibling); // move the text node into span } // catch clicks on whole tree tree.onclick = function(event) { if (event.target.tagName != 'SPAN') { return; } let childrenContainer = event.target.parentNode.querySelector('ul'); if (!childrenContainer) return; // no children childrenContainer.hidden = !childrenContainer.hidden; }
可排序的表格
使表格可排序:点击
<th>
元素,应按对应的列对表格进行排序。每个
<th>
的特性(attribute)中都有类型,如下所示:<table id="grid"> <thead> <tr> <th data-type="number">Age</th> <th data-type="string">Name</th> </tr> </thead> <tbody> <tr> <td>5</td> <td>John</td> </tr> <tr> <td>10</td> <td>Ann</td> </tr> ... </tbody> </table>
在上面的示例中,第一列为数字,第二列为字符串。排序函数应根据类型进行排序。
应该只支持
"string"
和"number"
类型。运行示例:
P.S. 表格可以更大,有任意数量的行和列。
document.addEventListener('click', event => { const target = event.target; const type = target.dataset.type; if (!type) return; const index = Array.from(grid.tHead.children[0].children).indexOf( target ); const list = Array.from(grid.tBodies[0].children); list.sort((a, b) => { let fun = null; if (type === 'string') fun = String; if (type === 'number') fun = Number; const one = fun(a.children[index].textContent); const two = fun(b.children[index].textContent); if (type === 'string') { console.log(one, two, two > one); return two > one ? 1 : -1; } return two - one; }); list.forEach(item => { grid.tBodies[0].prepend(item); }); }); // 参考解法 grid.onclick = function(e) { if (e.target.tagName != 'TH') return; let th = e.target; // if TH, then sort // cellIndex is the number of th: // 0 for the first column // 1 for the second column, etc sortGrid(th.cellIndex, th.dataset.type); }; function sortGrid(colNum, type) { let tbody = grid.querySelector('tbody'); let rowsArray = Array.from(tbody.rows); // compare(a, b) compares two rows, need for sorting let compare; switch (type) { case 'number': compare = function(rowA, rowB) { return rowA.cells[colNum].innerHTML - rowB.cells[colNum].innerHTML; }; break; case 'string': compare = function(rowA, rowB) { return rowA.cells[colNum].innerHTML > rowB.cells[colNum].innerHTML ? 1 : -1; }; break; } // sort rowsArray.sort(compare); tbody.append(...rowsArray); }
工具提示行为
编写工具提示(tooltip)行为的 JavaScript 代码。
当鼠标在带有
data-tooltip
的元素的上方时,工具提示应显示在其上方,当鼠标移开时,工具提示将隐藏起来。带有注释的 HTML 示例:
<button data-tooltip="the tooltip is longer than the element">Short button</button> <button data-tooltip="HTML<br>tooltip">One more button</button>
运行效果如下:
在此任务中,我们假设所有具有
data-tooltip
的元素中都只有文本。尚无嵌套标签。详情:
- 元素和工具提示之间的距离应为
5px
。 - 如果可能,工具提示应相对于元素居中。
- 工具提示不应与窗口边缘交叉。通常,它应该在元素的上方,但是如果元素位于页面顶部,并且没有工具提示的空间,则应该在元素的下方。
- 工具提示的内容在
data-tooltip
属性中给定。它可以是任意 HTML。
在这里你将需要两个事件:
mouseover
当鼠标指针出现在元素上方时触发。mouseout
当鼠标指针离开元素时触发。
请使用事件委托:在
document
上设置两个处理程序,以跟踪带有data-tooltip
的元素中的所有 “over” 和 “out”,并从那里管理工具提示。在实现了该行为后,即使不熟悉 JavaScript 的人也可以添加带注释的元素。
P.S. 一次只能显示一个工具提示。
let dom_tooltip = null; document.addEventListener('mouseover', event => { const target = event.target; const tooltip = target.dataset.tooltip; if (!tooltip) return; const rect = target.getBoundingClientRect(); console.log(rect.bottom); dom_tooltip = createToolTip(tooltip); const left = rect.left + (target.offsetWidth - dom_tooltip.offsetWidth) / 2; const top = rect.top - dom_tooltip.offsetHeight - 5; dom_tooltip.style.left = (left < 0 ? 0 : left) + 'px'; dom_tooltip.style.top = (top < 0 ? rect.bottom + 5 : top) + 'px'; document.body.append(dom_tooltip); }); document.addEventListener('mouseout', () => { const target = event.target; const tooltip = target.dataset.tooltip; if (!tooltip) return; dom_tooltip && dom_tooltip.remove(); }); function createToolTip(text) { const div = document.createElement('div'); div.innerHTML = text; div.classList.add('tooltip'); document.body.append(div); return div; } // 参考解法 let tooltipElem; document.onmouseover = function(event) { let target = event.target; // if we have tooltip HTML... let tooltipHtml = target.dataset.tooltip; if (!tooltipHtml) return; // ...create the tooltip element tooltipElem = document.createElement('div'); tooltipElem.className = 'tooltip'; tooltipElem.innerHTML = tooltipHtml; document.body.append(tooltipElem); // position it above the annotated element (top-center) let coords = target.getBoundingClientRect(); let left = coords.left + (target.offsetWidth - tooltipElem.offsetWidth) / 2; if (left < 0) left = 0; // don't cross the left window edge let top = coords.top - tooltipElem.offsetHeight - 5; if (top < 0) { // if crossing the top window edge, show below instead top = coords.top + target.offsetHeight + 5; } tooltipElem.style.left = left + 'px'; tooltipElem.style.top = top + 'px'; }; document.onmouseout = function(e) { if (tooltipElem) { tooltipElem.remove(); tooltipElem = null; } };
- 元素和工具提示之间的距离应为
浏览器默认行为
任务
捕获元素中的链接
使所有包含
id="contents"
的元素内的链接询问用户是否真的要离开。如果用户不想离开,那就不离开。像这样:
细节:
- 元素内的 HTML 可以被随时动态加载或重新生成,因此,我们无法找到所有链接并为其添加处理程序。这里使用事件委托。
- 内容中可能有嵌套的标签。链接中也是,例如
<a href=".."><i>...</i></a>
。
document.addEventListener('click', e => { const tar = e.target; const target = tar.closest('a'); if (!target) return; if (target.tagName !== 'A') return; const result = confirm('?'); if (!result) { e.preventDefault(); } }); // 参考解法 contents.onclick = function(event) { function handleLink(href) { let isLeaving = confirm(`Leave for ${href}?`); if (!isLeaving) return false; } let target = event.target.closest('a'); if (target && contents.contains(target)) { return handleLink(target.getAttribute('href')); } };
图册
创建一个图册,通过点击缩略图可以更改主图片。
像这样:
P.S. 使用事件委托。
thumbs.addEventListener('click', e => { e.preventDefault(); const target = e.target; if (target.tagName !== 'IMG') return; const href = target.parentNode.getAttribute('href'); console.log(target.parentNode); largeImg.setAttribute('src', href); }); // 参考解法 thumbs.onclick = function(event) { let thumbnail = event.target.closest('a'); if (!thumbnail) return; showThumbnail(thumbnail.href, thumbnail.title); event.preventDefault(); } function showThumbnail(href, title) { largeImg.src = href; largeImg.alt = title; }
鼠标事件
任务
可选列表
创建一个可以选择元素的列表,例如在文件管理器中。
- 点击列表元素,只选择该元素(添加
.selected
类),取消选择其他所有元素。 - 如果点击时,按键 Ctrl(在 Mac 中为 Cmd)是被按下的,则选择会被切换到被点击的元素上,但其他元素不会被改动。
示例:
P.S. 对于此任务,我们可以假设列表项是纯文本的。没有嵌套标签。
P.P.S. 防止点击时浏览器原生的文本选择。
const ul = document.querySelector('#ul'); const li = document.querySelectorAll('li'); ul.addEventListener('click', e => { const target = e.target; if (target.tagName !== 'LI') return; if (!e.ctrlKey && !e.metaKey) { li.forEach(l => { l.classList.remove('selected'); }); } target.classList.toggle('selected'); }); ul.onmousedown = function () { return false; }; // 参考解法 ul.onclick = function(event) { if (event.target.tagName != "LI") return; if (event.ctrlKey || event.metaKey) { toggleSelect(event.target); } else { singleSelect(event.target); } } // prevent unneeded selection of list elements on clicks ul.onmousedown = function() { return false; }; function toggleSelect(li) { li.classList.toggle('selected'); } function singleSelect(li) { let selected = ul.querySelectorAll('.selected'); for(let elem of selected) { elem.classList.remove('selected'); } li.classList.add('selected'); }
- 点击列表元素,只选择该元素(添加
移动鼠标:mouseover/out,mouseenter/leave
任务
改进的工具提示行为
编写 JavaScript,在带有
data-tooltip
特性(attribute)的元素上显示一个工具提示。该特性的值应该成为工具提示的文本。与任务 工具提示行为 类似,但这里可以嵌套带有注解(annotated)的元素。并且显示的是嵌套最深的工具提示。
同一时间只能显示一个工具提示。
例如:
<div data-tooltip="Here – is the house interior" id="house"> <div data-tooltip="Here – is the roof" id="roof"></div> ... <a href="https://en.wikipedia.org/wiki/The_Three_Little_Pigs" data-tooltip="Read on…">Hover over me</a> </div>
在 iframe 中的结果:
const house = document.querySelector('#house'); let oTooltip = null; house.addEventListener('mouseover', e => { const target = e.target.closest('[data-tooltip]'); if (!target) return; const tooltip = target.dataset.tooltip; oTooltip = createTooltip(tooltip); }); house.addEventListener('mouseout', e => { if (oTooltip) { oTooltip.remove(); oTooltip = null; } }); function createTooltip(text) { const div = document.createElement('div'); div.classList.add('tooltip'); div.textContent = text; document.body.append(div); return div; } // 参考解法 let tooltip; document.onmouseover = function(event) { // important: a fast-moving mouse may "jump" right to a child on an annotated node, skipping the parent // so mouseover may happen on a child. let anchorElem = event.target.closest('[data-tooltip]'); if (!anchorElem) return; // show tooltip and remember it tooltip = showTooltip(anchorElem, anchorElem.dataset.tooltip); } document.onmouseout = function() { // it is possible that mouseout triggered, but we're still inside the element // (its target was inside, and it bubbled) // but in this case we'll have an immediate mouseover, // so the tooltip will be destroyed and shown again // // luckily, the "blinking" won't be visible, // as both events happen almost at the same time if (tooltip) { tooltip.remove(); tooltip = false; } } function showTooltip(anchorElem, html) { let tooltipElem = document.createElement('div'); tooltipElem.className = 'tooltip'; tooltipElem.innerHTML = html; document.body.append(tooltipElem); let coords = anchorElem.getBoundingClientRect(); // position the tooltip over the center of the element let left = coords.left + (anchorElem.offsetWidth - tooltipElem.offsetWidth) / 2; if (left < 0) left = 0; let top = coords.top - tooltipElem.offsetHeight - 5; if (top < 0) { top = coords.top + anchorElem.offsetHeight + 5; } tooltipElem.style.left = left + 'px'; tooltipElem.style.top = top + 'px'; return tooltipElem; }
“智能”工具提示
编写一个函数,该函数仅在访问者将鼠标 移至 元素而不是 移过 元素的情况下,在该元素上显示工具提示。
换句话说,如果访问者将鼠标移至元素上,并停下来 —— 显示工具提示。如果他们只是将鼠标移过元素,那就没必要显示,谁想要多余的闪烁呢?
从技术上说,我们可以测量元素上的鼠标移动速度,如果速度很慢,那么我们就假定它 在元素上,并显示工具提示,如果速度很快 —— 那么我们就忽略它。
为此,我们创建一个通用对象
new HoverIntent(options)
。其
options
:elem
—— 要跟踪的元素。over
—— 鼠标移动到元素上时要调用的函数:即,鼠标在元素上的移动速度很慢,或者停在该元素上。out
—— 当鼠标离开元素时调用的函数(如果over
已经被调用过了)。
在工具提示中使用此类对象的示例:
// 一个简单的工具提示 let tooltip = document.createElement('div'); tooltip.className = "tooltip"; tooltip.innerHTML = "Tooltip"; // 该对象将跟踪鼠标,并调用 over/out new HoverIntent({ elem, over() { tooltip.style.left = elem.getBoundingClientRect().left + 'px'; tooltip.style.top = elem.getBoundingClientRect().bottom + 5 + 'px'; document.body.append(tooltip); }, out() { tooltip.remove(); } });
示例:
如果你将鼠标快速地从“时钟”上移动过去,那么什么都不会发生,如果你使用鼠标在“时钟”上慢慢移动,或者停在“时钟”上,则会出现一个工具提示。
请注意:当鼠标指针在“时钟”的元素之间移动时,工具提示不会“闪烁”
'use strict'; // 参考解法 class HoverIntent { constructor({ sensitivity = 0.1, // speed less than 0.1px/ms means "hovering over an element" interval = 100, // measure mouse speed once per 100ms elem, over, out }) { this.sensitivity = sensitivity; this.interval = interval; this.elem = elem; this.over = over; this.out = out; // make sure "this" is the object in event handlers. this.onMouseMove = this.onMouseMove.bind(this); this.onMouseOver = this.onMouseOver.bind(this); this.onMouseOut = this.onMouseOut.bind(this); // and in time-measuring function (called from setInterval) this.trackSpeed = this.trackSpeed.bind(this); elem.addEventListener("mouseover", this.onMouseOver); elem.addEventListener("mouseout", this.onMouseOut); } onMouseOver(event) { if (this.isOverElement) { // if we're over the element, then ignore the event // we are already measuring the speed return; } this.isOverElement = true; // after every mousemove we'll be check the distance // between the previous and the current mouse coordinates // if it's less than sensivity, then the speed is slow this.prevX = event.pageX; this.prevY = event.pageY; this.prevTime = Date.now(); elem.addEventListener('mousemove', this.onMouseMove); this.checkSpeedInterval = setInterval(this.trackSpeed, this.interval); } onMouseOut(event) { // if left the element if (!event.relatedTarget || !elem.contains(event.relatedTarget)) { this.isOverElement = false; this.elem.removeEventListener('mousemove', this.onMouseMove); clearInterval(this.checkSpeedInterval); if (this.isHover) { // if there was a stop over the element this.out.call(this.elem, event); this.isHover = false; } } } onMouseMove(event) { this.lastX = event.pageX; this.lastY = event.pageY; this.lastTime = Date.now(); } trackSpeed() { let speed; if (!this.lastTime || this.lastTime == this.prevTime) { // cursor didn't move speed = 0; } else { speed = Math.sqrt( Math.pow(this.prevX - this.lastX, 2) + Math.pow(this.prevY - this.lastY, 2) ) / (this.lastTime - this.prevTime); } if (speed < this.sensitivity) { clearInterval(this.checkSpeedInterval); this.isHover = true; this.over.call(this.elem, event); } else { // speed fast, remember new coordinates as the previous ones this.prevX = this.lastX; this.prevY = this.lastY; this.prevTime = this.lastTime; } } destroy() { elem.removeEventListener('mousemove', this.onMouseMove); elem.removeEventListener('mouseover', this.onMouseOver); elem.removeEventListener('mouseout', this.onMouseOut); } }