15.8.4 画布绘图操作
我们已经看到了一些基本的画布方法——beginPath()
、moveTo()
、lineTo()
、closePath()
、fill()
和 stroke()
——用于定义、填充和绘制线条和多边形。但 Canvas API 还包括其他绘图方法。
矩形
CanvasRenderingContext2D 定义了四种绘制矩形的方法。这四种矩形方法都需要两个参数,指定矩形的一个角,然后是矩形的宽度和高度。通常,您指定左上角,然后传递正宽度和正高度,但也可以指定其他角并传递负尺寸。
fillRect()
使用当前的fillStyle
填充指定的矩形。strokeRect()
使用当前的strokeStyle
和其他线条属性描绘指定矩形的轮廓。clearRect()
类似于fillRect()
,但它忽略当前的填充样式,并用透明黑色像素(所有空画布的默认颜色)填充矩形。这三种方法的重要之处在于它们不会影响当前路径或路径中的当前点。
最后一个矩形方法被命名为rect()
,它会影响当前路径:它将指定的矩形添加到路径的子路径中。与其他定义路径方法一样,它本身不填充或描边任何内容。
曲线
路径是子路径的序列,子路径是连接点的序列。在我们在§15.8.1 中定义的路径中,这些点是用直线段连接的,但这并不总是这样。CanvasRenderingContext2D 对象定义了许多方法,这些方法向子路径添加一个新点,并使用曲线将当前点连接到该新点:
arc()
这种方法向路径中添加一个圆或圆的一部分(弧)。要绘制的圆弧由六个参数指定:圆的中心的x和y坐标,圆的半径,圆弧的起始和结束角度,以及这两个角度之间的圆弧的方向(顺时针或逆时针)。如果路径中有当前点,则此方法将当前点与圆弧的起始点用一条直线连接(在绘制楔形或饼状图时很有用),然后将圆弧的起始点与圆弧的结束点用一部分圆连接起来,将圆弧的结束点作为新的当前点。如果在调用此方法时没有当前点,则它只会将圆弧添加到路径中。
ellipse()
这种方法与arc()
非常相似,但它向路径中添加一个椭圆或椭圆的一部分。它有两个半径而不是一个:一个x轴半径和一个y轴半径。此外,由于椭圆不是径向对称的,因此此方法需要另一个参数,指定椭圆围绕其中心顺时针旋转的弧度数。
arcTo()
这种方法绘制一条直线和一个圆弧,就像arc()
方法一样,但它使用不同的参数指定要绘制的圆弧。arcTo()
的参数指定了点 P1 和 P2 以及半径。添加到路径中的圆弧具有指定的半径。它从当前点到 P1 的切线点开始,并在 P1 和 P2 之间的(虚拟)线的切线点结束。这种看似不寻常的指定圆弧的方法实际上非常有用,用于绘制具有圆角的形状。如果指定半径为 0,此方法只会从当前点画一条直线到 P1。然而,如果半径不为零,则它会从当前点沿着 P1 的方向画一条直线,然后将该线围绕成一个圆,直到指向 P2 的方向。
bezierCurveTo()
这种方法向子路径添加一个新点 P,并使用三次贝塞尔曲线将其连接到当前点。曲线的形状由两个“控制点”C1 和 C2 指定。在曲线的起始点(当前点处),曲线朝向 C1 的方向。在曲线的结束点(点 P 处),曲线从 C2 的方向到达。在这些点之间,曲线的方向平滑变化。点 P 成为子路径的新当前点。
quadraticCurveTo()
这种方法类似于bezierCurveTo()
,但它使用二次贝塞尔曲线而不是三次贝塞尔曲线,并且只有一个控制点。
您可以使用这些方法绘制类似于图 15-10 中的路径。
图 15-10。画布中的曲线路径
示例 15-6 显示了用于创建图 15-10 的代码。此代码中演示的方法是 Canvas API 中最复杂的方法之一;请参考在线参考资料以获取有关这些方法及其参数的完整详细信息。
示例 15-6。向路径添加曲线
// A utility function to convert angles from degrees to radians function rads(x) { return Math.PI*x/180; } // Get the context object of the document's canvas element let c = document.querySelector("canvas").getContext("2d"); // Define some graphics attributes and draw the curves c.fillStyle = "#aaa"; // Gray fills c.lineWidth = 2; // 2-pixel black (by default) lines // Draw a circle. // There is no current point, so draw just the circle with no straight // line from the current point to the start of the circle. c.beginPath(); c.arc(75,100,50, // Center at (75,100), radius 50 0,rads(360),false); // Go clockwise from 0 to 360 degrees c.fill(); // Fill the circle c.stroke(); // Stroke its outline. // Now draw an ellipse in the same way c.beginPath(); // Start new path not connected to the circle c.ellipse(200, 100, 50, 35, rads(15), // Center, radii, and rotation 0, rads(360), false); // Start angle, end angle, direction // Draw a wedge. Angles are measured clockwise from the positive x axis. // Note that arc() adds a line from the current point to the arc start. c.moveTo(325, 100); // Start at the center of the circle. c.arc(325, 100, 50, // Circle center and radius rads(-60), rads(0), // Start at angle -60 and go to angle 0 true); // counterclockwise c.closePath(); // Add radius back to the center of the circle // Similar wedge, offset a bit, and in the opposite direction c.moveTo(340, 92); c.arc(340, 92, 42, rads(-60), rads(0), false); c.closePath(); // Use arcTo() for rounded corners. Here we draw a square with // upper left corner at (400,50) and corners of varying radii. c.moveTo(450, 50); // Begin in the middle of the top edge. c.arcTo(500,50,500,150,30); // Add part of top edge and upper right corner. c.arcTo(500,150,400,150,20); // Add right edge and lower right corner. c.arcTo(400,150,400,50,10); // Add bottom edge and lower left corner. c.arcTo(400,50,500,50,0); // Add left edge and upper left corner. c.closePath(); // Close path to add the rest of the top edge. // Quadratic Bezier curve: one control point c.moveTo(525, 125); // Begin here c.quadraticCurveTo(550, 75, 625, 125); // Draw a curve to (625, 125) c.fillRect(550-3, 75-3, 6, 6); // Mark the control point (550,75) // Cubic Bezier curve c.moveTo(625, 100); // Start at (625, 100) c.bezierCurveTo(645,70,705,130,725,100); // Curve to (725, 100) c.fillRect(645-3, 70-3, 6, 6); // Mark control points c.fillRect(705-3, 130-3, 6, 6); // Finally, fill the curves and stroke their outlines. c.fill(); c.stroke();
文本
要在画布中绘制文本,通常使用fillText()
方法,该方法使用fillStyle
属性指定的颜色(或渐变或图案)绘制文本。对于大文本尺寸的特殊效果,可以使用strokeText()
绘制单个字体字形的轮廓。这两种方法的第一个参数是要绘制的文本,第二个和第三个参数是文本的x和y坐标。这两种方法都不会影响当前路径或当前点。
fillText()
和strokeText()
接受一个可选的第四个参数。如果提供了这个参数,则指定要显示的文本的最大宽度。如果使用font
属性绘制的文本宽度超过指定值,画布将通过缩放或使用更窄或更小的字体来适应它。
如果需要在绘制文本之前自行测量文本,请将其传递给measureText()
方法。该方法返回一个指定使用当前font
绘制时文本测量的 TextMetrics 对象。在撰写本文时,TextMetrics
对象中唯一包含的“度量”是宽度。像这样查询字符串的屏幕宽度:
let width = c.measureText(text).width;
如果您想在画布中居中显示一串文本,这将非常有用。
图像
除了矢量图形(路径、线条等)外,Canvas API 还支持位图图像。drawImage()
方法将源图像的像素(或源图像内的矩形)复制到画布上,并根据需要对图像的像素进行缩放和旋转。
drawImage()
可以使用三、五或九个参数调用。在所有情况下,第一个参数都是要复制像素的源图像。这个图像参数通常是一个元素,但也可以是另一个
元素,甚至是一个
元素(从中将复制一帧)。如果指定的或
元素仍在加载数据,则drawImage()
调用将不起作用。
在drawImage()
的三参数版本中,第二个和第三个参数指定要绘制图像左上角的x和y坐标。在此方法的版本中,整个源图像都会被复制到画布上。x和y坐标在当前坐标系中解释,并且根据当前生效的画布变换,必要时会对图像进行缩放和旋转。
drawImage()
的五参数版本在前述的x
和y
参数中添加了width
和height
参数。这四个参数定义了画布内的目标矩形。源图像的左上角位于(x,y)
,右下角位于(x+width, y+height)
。同样,整个源图像都会被复制。使用此方法的版本,源图像将被缩放以适应目标矩形。
drawImage()
的九参数版本同时指定源矩形和目标矩形,并仅复制源矩形内的像素。第二至第五个参数指定源矩形,它们以 CSS 像素为单位。如果源图像是另一个画布,则源矩形使用该画布的默认坐标系,并忽略已指定的任何变换。第六至第九个参数指定将绘制图像的目标矩形,并且以画布的当前坐标系而不是默认坐标系为准。
除了将图像绘制到画布中,我们还可以使用 toDataURL()
方法将画布的内容提取为图像。与这里描述的所有其他方法不同,toDataURL()
是 Canvas 元素本身的方法,而不是上下文对象的方法。通常不带参数调用 toDataURL()
,它会将画布的内容作为 PNG 图像编码为字符串返回,使用 data:
URL。返回的 URL 适用于 元素的使用,您可以使用类似以下代码对画布进行静态快照:
let img = document.createElement("img"); // Create an <img> element img.src = canvas.toDataURL(); // Set its src attribute document.body.appendChild(img); // Append it to the document
15.8.5 坐标系变换
正如我们所指出的,画布的默认坐标系将原点放在左上角,x 坐标向右增加,y 坐标向下增加。在此默认系统中,点的坐标直接映射到 CSS 像素(然后直接映射到一个或多个设备像素)。某些画布操作和属性(例如提取原始像素值和设置阴影偏移)始终使用此默认坐标系。除了默认坐标系外,每个画布还有一个“当前变换矩阵”作为其图形状态的一部分。该矩阵定义了画布的当前坐标系。在大多数画布操作中,当您指定点的坐标时,它被视为当前坐标系中的点,而不是默认坐标系中的点。当前变换矩阵用于将您指定的坐标转换为默认坐标系中的等效坐标。
setTransform()
方法允许您直接设置画布的变换矩阵,但坐标系变换通常更容易指定为一系列平移、旋转和缩放操作。图 15-11 说明了这些操作及其对画布坐标系的影响。生成该图的程序连续七次绘制了相同的坐标轴。每次变化的唯一事物是当前变换。请注意,变换不仅影响绘制的线条,还影响文本。
图 15-11. 坐标系变换
translate()
方法简单地将坐标系的原点向左、向右、向上或向下移动。rotate()
方法按指定角度顺时针旋转坐标轴。(Canvas API 总是用弧度指定角度。要将度数转换为弧度,除以 180 并乘以 Math.PI
。)scale()
方法沿着 x 或 y 轴拉伸或收缩距离。
将负的比例因子传递给 scale()
方法会使该轴在原点处翻转,就像在镜子中反射一样。这就是在 图 15-11 的左下角所做的事情:translate()
用于将原点移动到画布的左下角,然后 scale()
用于翻转 y 轴,使得随着页面向上移动,y 坐标增加。这样的翻转坐标系在代数课上很常见,可能对绘制图表上的数据点有用。但请注意,这会使文本难以阅读!
数学上理解变换
我发现最容易理解变换的方法是几何上的,将 translate()
、rotate()
和 scale()
视为转换坐标系的轴,如 图 15-11 所示。也可以将变换理解为代数方程,这些方程将变换后坐标系中点 (x,y)
的坐标映射回先前坐标系中相同点 (x',y')
的坐标。
方法调用 c.translate(dx,dy)
可以用以下方程描述:
x' = x + dx; // An X coordinate of 0 in the new system is dx in the old y' = y + dy;
缩放操作有类似简单的方程。调用 c.scale(sx,sy)
可以描述如下:
x' = sx * x; y' = sy * y;
旋转更加复杂。调用 c.rotate(a)
由以下三角函数方程描述:
x' = x * cos(a) - y * sin(a); y' = y * cos(a) + x * sin(a);
注意变换的顺序很重要。 假设我们从画布的默认坐标系开始,然后将其平移,然后缩放。 为了将当前坐标系中的点(x,y)
映射回默认坐标系中的点(x'',y'')
,我们必须首先应用缩放方程将点映射到平移但未缩放的坐标系中的中间点(x',y')
,然后使用平移方程从这个中间点映射到(x'',y'')
。 结果如下:
x'' = sx*x + dx; y'' = sy*y + dy;
另一方面,如果我们在调用translate()
之前调用了scale()
,则得到的方程将不同:
x'' = sx*(x + dx); y'' = sy*(y + dy);
在代数上考虑变换序列时,要记住的关键是必须从最后(最近)的变换向前工作到第一个。 然而,在几何上考虑变换的轴时,您从第一个变换向最后一个变换工作。
画布支持的变换称为仿射变换。 仿射变换可以修改点之间的距离和线之间的角度,但平行线在仿射变换后始终保持平行——例如,不可能用仿射变换指定鱼眼镜头畸变。 任意仿射变换可以用这些方程中的六个参数a
到f
来描述:
x' = ax + cy + e y' = bx + dy + f
您可以通过将这六个参数传递给transform()
方法,对当前坐标系应用任意变换。 图 15-11 展示了两种类型的变换——倾斜和围绕指定点旋转——您可以像这样使用transform()
方法实现:
// Shear transform: // x' = x + kx*y; // y' = ky*x + y; function shear(c, kx, ky) { c.transform(1, ky, kx, 1, 0, 0); } // Rotate theta radians counterclockwise around the point (x,y) // This can also be accomplished with a translate, rotate, translate sequence function rotateAbout(c, theta, x, y) { let ct = Math.cos(theta); let st = Math.sin(theta); c.transform(ct, -st, st, ct, -x*ct-y*st+x, x*st-y*ct+y); }
setTransform()
方法接受与transform()
相同的参数,但是不是转换当前坐标系,而是忽略当前系统,转换默认坐标系,并使结果成为新的当前坐标系。 setTransform()
对于临时将画布重置为其默认坐标系很有用:
c.save(); // Save current coordinate system c.setTransform(1,0,0,1,0,0); // Revert to the default coordinate system // Perform operations using default CSS pixel coordinates c.restore(); // Restore the saved coordinate system
变换示例
示例 15-7 通过递归使用translate()
、rotate()
和scale()
方法来绘制科赫雪花分形图,展示了坐标系变换的强大功能。 此示例的输出显示在图 15-12 中,显示了具有 0、1、2、3 和 4 个递归级别的科赫雪花。
图 15-12. 科赫雪花
生成这些图形的代码很简洁,但其使用递归坐标系变换使其有些难以理解。 即使您不理解所有细微之处,也请注意代码中仅包含一次对lineTo()
方法的调用。 图 15-12 中的每个线段都是这样绘制的:
c.lineTo(len, 0);
变量len
的值在程序执行过程中不会改变,因此每个线段的位置、方向和长度由平移、旋转和缩放操作确定。
示例 15-7. 具有变换的科赫雪花
let deg = Math.PI/180; // For converting degrees to radians // Draw a level-n Koch snowflake fractal on the canvas context c, // with lower-left corner at (x,y) and side length len. function snowflake(c, n, x, y, len) { c.save(); // Save current transformation c.translate(x,y); // Translate origin to starting point c.moveTo(0,0); // Begin a new subpath at the new origin leg(n); // Draw the first leg of the snowflake c.rotate(-120*deg); // Now rotate 120 degrees counterclockwise leg(n); // Draw the second leg c.rotate(-120*deg); // Rotate again leg(n); // Draw the final leg c.closePath(); // Close the subpath c.restore(); // And restore original transformation // Draw a single leg of a level-n Koch snowflake. // This function leaves the current point at the end of the leg it has // drawn and translates the coordinate system so the current point is (0,0). // This means you can easily call rotate() after drawing a leg. function leg(n) { c.save(); // Save the current transformation if (n === 0) { // Nonrecursive case: c.lineTo(len, 0); // Just draw a horizontal line } // _ _ else { // Recursive case: draw 4 sub-legs like: \/ c.scale(1/3,1/3); // Sub-legs are 1/3 the size of this leg leg(n-1); // Recurse for the first sub-leg c.rotate(60*deg); // Turn 60 degrees clockwise leg(n-1); // Second sub-leg c.rotate(-120*deg); // Rotate 120 degrees back leg(n-1); // Third sub-leg c.rotate(60*deg); // Rotate back to our original heading leg(n-1); // Final sub-leg } c.restore(); // Restore the transformation c.translate(len, 0); // But translate to make end of leg (0,0) } } let c = document.querySelector("canvas").getContext("2d"); snowflake(c, 0, 25, 125, 125); // A level-0 snowflake is a triangle snowflake(c, 1, 175, 125, 125); // A level-1 snowflake is a 6-sided star snowflake(c, 2, 325, 125, 125); // etc. snowflake(c, 3, 475, 125, 125); snowflake(c, 4, 625, 125, 125); // A level-4 snowflake looks like a snowflake! c.stroke(); // Stroke this very complicated path
15.8.6 裁剪
定义路径后,通常会调用stroke()
或fill()
(或两者)。 您还可以调用clip()
方法来定义裁剪区域。 一旦定义了裁剪区域,就不会在其外部绘制任何内容。 图 15-13 展示了使用裁剪区域生成的复杂图形。 图中垂直条纹沿中间运行,底部的文本是在定义三角形裁剪区域之后未裁剪的描边,然后填充的。
图 15-13. 未裁剪的笔画和裁剪的填充
图 15-13 是使用示例 15-5 的polygon()
方法和以下代码生成的:
// Define some drawing attributes c.font = "bold 60pt sans-serif"; // Big font c.lineWidth = 2; // Narrow lines c.strokeStyle = "#000"; // Black lines // Outline a rectangle and some text c.strokeRect(175, 25, 50, 325); // A vertical stripe down the middle c.strokeText("<canvas>", 15, 330); // Note strokeText() instead of fillText() // Define a complex path with an interior that is outside. polygon(c,3,200,225,200); // Large triangle polygon(c,3,200,225,100,0,true); // Smaller reverse triangle inside // Make that path the clipping region. c.clip(); // Stroke the path with a 5 pixel line, entirely inside the clipping region. c.lineWidth = 10; // Half of this 10 pixel line will be clipped away c.stroke(); // Fill the parts of the rectangle and text that are inside the clipping region c.fillStyle = "#aaa"; // Light gray c.fillRect(175, 25, 50, 325); // Fill the vertical stripe c.fillStyle = "#888"; // Darker gray c.fillText("<canvas>", 15, 330); // Fill the text
需要注意的是,当你调用clip()
时,当前路径本身会被剪切到当前剪切区域,然后被剪切的路径成为新的剪切区域。这意味着clip()
方法可以缩小剪切区域,但不能扩大它。没有方法可以重置剪切区域,因此在调用clip()
之前,通常应该调用save()
,这样以后就可以restore()
未剪切的区域。
15.8.7 像素处理
getImageData()
方法返回一个表示画布矩形区域的原始像素(作为 R、G、B 和 A 分量)的 ImageData 对象。您可以使用createImageData()
创建空的ImageData
对象。ImageData 对象中的像素是可写的,因此您可以按照自己的方式设置它们,然后使用putImageData()
将这些像素复制回画布。
这些像素处理方法提供了对画布的非常低级访问。您传递给getImageData()
的矩形位于默认坐标系统中:其尺寸以 CSS 像素为单位,不受当前变换的影响。当您调用putImageData()
时,您指定的位置也是以默认坐标系统中的尺寸来衡量的。此外,putImageData()
忽略所有图形属性。它不执行任何合成,不将像素乘以globalAlpha
,也不绘制阴影。
像素处理方法对于实现图像处理非常有用。示例 15-8 展示了如何创建一个简单的运动模糊或“涂抹”效果,就像图 15-14 中显示的那样。
图 15-14. 通过图像处理创建的运动模糊效果
以下代码演示了getImageData()
和putImageData()
,并展示了如何迭代并修改 ImageData 对象中的像素值。
示例 15-8. 使用 ImageData 进行运动模糊
// Smear the pixels of the rectangle to the right, producing a // sort of motion blur as if objects are moving from right to left. // n must be 2 or larger. Larger values produce bigger smears. // The rectangle is specified in the default coordinate system. function smear(c, n, x, y, w, h) { // Get the ImageData object that represents the rectangle of pixels to smear let pixels = c.getImageData(x, y, w, h); // This smear is done in-place and requires only the source ImageData. // Some image processing algorithms require an additional ImageData to // store transformed pixel values. If we needed an output buffer, we could // create a new ImageData with the same dimensions like this: // let output_pixels = c.createImageData(pixels); // Get the dimensions of the grid of pixels in the ImageData object let width = pixels.width, height = pixels.height; // This is the byte array that holds the raw pixel data, left-to-right and // top-to-bottom. Each pixel occupies 4 consecutive bytes in R,G,B,A order. let data = pixels.data; // Each pixel after the first in each row is smeared by replacing it with // 1/nth of its own value plus m/nths of the previous pixel's value let m = n-1; for(let row = 0; row < height; row++) { // For each row let i = row*width*4 + 4; // The offset of the second pixel of the row for(let col = 1; col < width; col++, i += 4) { // For each column data[i] = (data[i] + data[i-4]*m)/n; // Red pixel component data[i+1] = (data[i+1] + data[i-3]*m)/n; // Green data[i+2] = (data[i+2] + data[i-2]*m)/n; // Blue data[i+3] = (data[i+3] + data[i-1]*m)/n; // Alpha component } } // Now copy the smeared image data back to the same position on the canvas c.putImageData(pixels, x, y); }
15.9 音频 API
HTML 和
标签允许您轻松地在网页中包含声音和视频。这些是具有重要 API 和复杂用户界面的复杂元素。您可以使用play()
和pause()
方法控制媒体播放。您可以设置volume
和playbackRate
属性来控制音频音量和播放速度。您可以通过设置currentTime
属性跳转到媒体中的特定时间。
我们不会在这里进一步详细介绍和
标签。以下小节演示了两种向网页添加脚本化声音效果的方法。
15.9.1 Audio()构造函数
您不必在 HTML 文档中包含标签以在网页中包含声音效果。您可以使用普通的 DOM
document.createElement()
方法动态创建元素,或者作为快捷方式,您可以简单地使用
Audio()
构造函数。您不必将创建的元素添加到文档中才能播放它。您只需调用其play()
方法即可:
// Load the sound effect in advance so it is ready for use let soundeffect = new Audio("soundeffect.mp3"); // Play the sound effect whenever the user clicks the mouse button document.addEventListener("click", () => { soundeffect.cloneNode().play(); // Load and play the sound });
注意这里使用了cloneNode()
。如果用户快速点击鼠标,我们希望能够同时播放多个重叠的声音效果副本。为了做到这一点,我们需要多个音频元素。因为音频元素没有添加到文档中,所以当它们播放完毕时会被垃圾回收。
15.9.2 WebAudio API
除了使用 Audio 元素播放录制的声音外,Web 浏览器还允许使用 WebAudio API 生成和播放合成声音。使用 WebAudio API 就像连接旧式电子合成器的插线一样。使用 WebAudio,您创建一组 AudioNode 对象,表示波形的源、变换或目的地,然后将这些节点连接到一个网络中以产生声音。API 并不特别复杂,但要全面解释需要理解超出本书范围的电子音乐和信号处理概念。
下面的代码示例使用 WebAudio API 合成一个在大约一秒钟内淡出的短和弦。这个示例演示了 WebAudio API 的基础知识。如果你对此感兴趣,你可以在网上找到更多关于这个 API 的信息:
// Begin by creating an audioContext object. Safari still requires // us to use webkitAudioContext instead of AudioContext. let audioContext = new (this.AudioContext||this.webkitAudioContext)(); // Define the base sound as a combination of three pure sine waves let notes = [ 293.7, 370.0, 440.0 ]; // D major chord: D, F# and A // Create oscillator nodes for each of the notes we want to play let oscillators = notes.map(note => { let o = audioContext.createOscillator(); o.frequency.value = note; return o; }); // Shape the sound by controlling its volume over time. // Starting at time 0 quickly ramp up to full volume. // Then starting at time 0.1 slowly ramp down to 0. let volumeControl = audioContext.createGain(); volumeControl.gain.setTargetAtTime(1, 0.0, 0.02); volumeControl.gain.setTargetAtTime(0, 0.1, 0.2); // We're going to send the sound to the default destination: // the user's speakers let speakers = audioContext.destination; // Connect each of the source notes to the volume control oscillators.forEach(o => o.connect(volumeControl)); // And connect the output of the volume control to the speakers. volumeControl.connect(speakers); // Now start playing the sounds and let them run for 1.25 seconds. let startTime = audioContext.currentTime; let stopTime = startTime + 1.25; oscillators.forEach(o => { o.start(startTime); o.stop(stopTime); }); // If we want to create a sequence of sounds we can use event handlers oscillators[0].addEventListener("ended", () => { // This event handler is invoked when the note stops playing });
15.10 位置、导航和历史
location
属性既可以用于 Window 对象,也可以用于 Document 对象,它指的是 Location 对象,代表着窗口中显示的文档的当前 URL,并提供了一个 API 用于在窗口中加载新文档。
Location 对象非常类似于 URL 对象(§11.9),你可以使用protocol
、hostname
、port
和path
等属性来访问当前文档的 URL 的各个部分。href
属性返回整个 URL 作为字符串,toString()
方法也是如此。
Location 对象的hash
和search
属性是比较有趣的。hash
属性返回 URL 中的“片段标识符”部分,如果有的话:一个井号(#)后跟一个元素 ID。search
属性类似。它返回以问号开头的 URL 部分:通常是某种查询字符串。一般来说,URL 的这部分用于对 URL 进行参数化,并提供了一种在其中嵌入参数的方式。虽然这些参数通常是为在服务器上运行的脚本而设计的,但也可以在启用 JavaScript 的页面中使用。
URL 对象有一个searchParams
属性,它是search
属性的解析表示。Location 对象没有searchParams
属性,但如果你想解析window.location.search
,你可以简单地从 Location 对象创建一个 URL 对象,然后使用 URL 的searchParams
:
let url = new URL(window.location); let query = url.searchParams.get("q"); let numResults = parseInt(url.searchParams.get("n") || "10");
除了可以引用为window.location
或document.location
的 Location 对象,以及我们之前使用的URL()
构造函数,浏览器还定义了一个document.URL
属性。令人惊讶的是,这个属性的值不是一个 URL 对象,而只是一个字符串。该字符串保存当前文档的 URL。
15.10.1 加载新文档
如果你将一个字符串分配给window.location
或document.location
,那么该字符串会被解释为一个 URL,浏览器会加载它,用新文档替换当前文档:
window.location = "http://www.oreilly.com"; // Go buy some books!
你也可以将相对 URL 分配给location
。它们相对于当前 URL 解析:
document.location = "page2.html"; // Load the next page
一个裸的片段标识符是一种特殊类型的相对 URL,它不会导致浏览器加载新文档,而只是滚动,以使具有与片段匹配的id
或name
的文档元素在浏览器窗口顶部可见。作为一个特例,片段标识符#top
会使浏览器跳转到文档的开头(假设没有元素具有id="top"
属性):
location = "#top"; // Jump to the top of the document
Location 对象的各个属性都是可写的,设置它们会改变位置 URL,并导致浏览器加载一个新文档(或者在hash
属性的情况下,在当前文档内导航):
document.location.path = "pages/3.html"; // Load a new page document.location.hash = "TOC"; // Scroll to the table of contents location.search = "?page=" + (page+1); // Reload with new query string
你也可以通过向 Location 对象的assign()
方法传递一个新字符串来加载新页面。这与将字符串分配给location
属性相同,因此并不特别有趣。
另一方面,Location 对象的replace()
方法非常有用。当你向replace()
传递一个字符串时,它被解释为一个 URL,并导致浏览器加载一个新页面,就像assign()
一样。不同之处在于replace()
替换了浏览器历史中的当前文档。如果文档 A 中的脚本设置location
属性或调用assign()
加载文档 B,然后用户点击返回按钮,浏览器将返回到文档 A。如果你使用replace()
,那么文档 A 将从浏览器历史中删除,当用户点击返回按钮时,浏览器将返回到在显示文档 A 之前显示的文档。
当脚本无条件加载新文档时,replace()
方法比 assign()
更好。否则,点击返回按钮会将浏览器返回到原始文档,并且同样的脚本会再次加载新文档。假设你有一个使用 JavaScript 增强的页面版本和一个不使用 JavaScript 的静态版本。如果确定用户的浏览器不支持你想要使用的 Web 平台 API,你可以使用 location.replace()
来加载静态版本:
// If the browser does not support the JavaScript APIs we need, // redirect to a static page that does not use JavaScript. if (!isBrowserSupported()) location.replace("staticpage.html");
注意,传递给 replace()
的 URL 是相对的。相对 URL 被解释为相对于其出现的页面,就像它们在超链接中使用时一样。
除了 assign()
和 replace()
方法,Location 对象还定义了 reload()
,它简单地使浏览器重新加载文档。
15.10.2 浏览历史
Window 对象的 history
属性指的是窗口的 History 对象。History 对象将窗口的浏览历史建模为文档和文档状态的列表。History 对象的 length
属性指定浏览历史列表中的元素数量,但出于安全原因,脚本不允许访问存储的 URL。(如果允许,任何脚本都可以窥探您的浏览历史。)
History 对象有 back()
和 forward()
方法,行为类似于浏览器的返回和前进按钮:它们使浏览器在其浏览历史中向后或向前移动一步。第三个方法 go()
接受一个整数参数,可以在历史记录列表中跳过任意数量的页面向前(对于正参数)或向后(对于负参数):
history.go(-2); // Go back 2, like clicking the Back button twice history.go(0); // Another way to reload the current page
如果一个窗口包含子窗口(如 <iframe> 元素),子窗口的浏览历史与主窗口的历史按时间顺序交错。这意味着在主窗口上调用 history.back()(例如)可能会导致其中一个子窗口导航回到先前显示的文档,但保持主窗口处于当前状态。
这里描述的 History 对象可以追溯到 Web 早期,当时文档是被动的,所有计算都在服务器上执行。如今,Web 应用程序经常动态生成或加载内容,并显示新的应用程序状态,而不实际加载新文档。如果这样的应用程序希望用户能够使用返回和前进按钮(或等效手势)以直观的方式从一个应用程序状态导航到另一个状态,它们必须执行自己的历史管理。有两种方法可以实现这一点,将在接下来的两个部分中描述。
JavaScript 权威指南第七版(GPT 重译)(六)(4)https://developer.aliyun.com/article/1485428