Canvas之鼠标滑动特效

大家好,我是皮皮。

我们会看到很多网页的粒子特效;如上图所示,这些都是借助HTML新特性,使用新增标签Canvas得到的效果;那么我们来了解下canvas。

什么是 Canvas

在 MDN 中是这样定义 <canvas> 的:

<canvas> 是 HTML5 新增的元素,可用于通过使用 JavaScript 中的脚本来绘制图形。例如,它可以用于绘制图形、制作照片、创建动画,甚至可以进行实时视频处理或渲染。

这里需要划重点的是,<canvas> 只是一个画布,本身并不具有绘图的能力,绘图必须使用 JavaScript 等脚本语言。

<canvas> 标签允许脚本语言动态渲染位图像。<canvas> 标签创建出了一个可绘制区域,JavaScript 代码可以通过一套完整的绘图功能类似于其他通用二维的 API 访问该区域,从而生成动态的图形。

我们可以认为 <canvas> 标签只是一个矩形的画布。JavaScript 就是画笔,负责在画布上画画。

Canvas 解决了什么问题?

在互联网出现的早期,Web 只不过是静态文本和链接的集合。1993 年,有人提出了 img 标签,它可以用来嵌入图像。

由于互联网的发展越来越迅猛,Web 应用已经从 Web 文档发展到 Web 应用程序。但是图像一直是静态的,人们越来越希望在其网站和应用程序中使用动态媒体(如音频、视频和交互式动画等),于是 Flash 就出现了。

但是随着 Web 应用的发展,出现了 HTML5,在 HTML5 中,浏览器中的媒体元素大受青睐。包括出现新的 Audio 和 Video 标签,可以直接将音频和视频资源放在 Web 上,而不需要其他第三方。

其次就是为了解决只能在 Web 页面中显示静态图片的问题,出现了 Canvas 标签。它是一个绘图表面,包含一组丰富的 JavaScript API,这些 API 使你能够动态创建和操作图像及动画。img 对静态图形内容起到了哪些作用,Canvas 就可能对可编写脚本的动态内容起到哪些作用。

案例-鼠标滑动效果

了解了canvas之后,我们使用新标签搭配原生JS,实现一个简单的页面特效 —— 鼠标滑动效果,如下所示:

页面搭建

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <canvas id="myCanvas"></canvas>
</body>

</html>

页面非常简单,只需要一个简单的canvas标签即可;

样式设置

* {
    margin: 0;
    padding: 0;
}

body {
    overflow: hidden;
}

#myCanvas {
    background-color: #000; /* 黑色 */
}

逻辑交互

初始化设置

var myCanvas = document.getElementById('myCanvas');
var ctx = myCanvas.getContext("2d");
var starlist = [];
function init() {
    // 设置canvas区域的范围为整个页面
    myCanvas.width = window.innerWidth;
    myCanvas.height = window.innerHeight;
};
init();

监听

window.onresize = init; // 监听屏幕大小改变 重新为canvas大小赋值

移动事件

// 当鼠标移动时 将鼠标坐标传入构造函数 同时创建一个对象
myCanvas.addEventListener('mousemove', function (e) {
    // 将对象push到数组中,画出来的彩色小点可以看作每一个对象中记录着信息 然后存在数组中
    starlist.push(new Star(e.offsetX, e.offsetY));
});

随机函数

// 随机函数封装,设置坐标
function random(min, max) {
    // 设置生成随机数公式
    return Math.floor((max - min) * Math.random() + min);
};

对象构造

// 定义了一个构造函数进行对象构造
function Star(x, y) {
    // 将坐标存在每一个点的对象中
    this.x = x;
    this.y = y;
    // 设置随机偏移量
    this.vx = (Math.random() - 0.5) * 3;
    this.vy = (Math.random() - 0.5) * 3;
    this.color = 'rgb(' + random(0, 256) + ',' + random(0, 256) + ',' + random(0, 256) + ')';
    this.a = 1; // 初始透明度
    this.draw(); // 把对象绘制到页面
}

方法封装

//star对象原型上封装方法
Star.prototype = {
    // canvas根据数组中存在的每一个对象的小点信息开始画
    draw: function () {
        ctx.beginPath();
        ctx.fillStyle = this.color;
        // 图像覆盖  显示方式 lighter 会将覆盖部分的颜色重叠显示出来
        ctx.globalCompositeOperation = 'lighter'
        ctx.globalAlpha = this.a;
        ctx.arc(this.x, this.y, 30, 0, Math.PI * 2, false);
        ctx.fill();
        this.updata();
    },
    updata() {
        // 根据偏移量更新每一个小点的位置
        this.x += this.vx;
        this.y += this.vy;
        // 透明度越来越小
        this.a *= 0.98;
    }
}

渲染

// 将小球渲染到页面上
function render() {
    // 每一次根据改变后数组中的元素进行画圆圈  把原来的内容区域清除掉
    ctx.clearRect(0, 0, myCanvas.width, myCanvas.height)

    // 根据存在数组中的每一位对象中的信息画圆
    starlist.forEach(function (ele, i) {
        ele.draw();
        // 如果数组中存在透明度小的对象 ,给他去掉 效果展示逐渐消失
        if (ele.a < 0.05) {
            starlist.splice(i, 1);
        }
    });
    requestAnimationFrame(render);
}
render();

总结

本文我们首先了解了粒子特效,它大多数是HTML的新特性,使用canvas标签创建出来的,或者说是绘制吧,canvas是画布,但是还需要搭配js来构造动态效果,创建出好看的特效。总而言之,canvas的出现使得前端页面变得更加丰富多彩,提高了视觉效果,对于用户体验进行了适当的增强,感兴趣的小伙伴可以去尝试下,鼠标滑动效果特效。

小伙伴们,快快用实践一下吧!如果在学习过程中,有遇到任何问题,欢迎加我好友,我拉你进Python学习交流群共同探讨学习。

一篇文章带你搞定JavaScript 性能调优

大家好,我是皮皮。

JavaScript 是单线程运行的,所以在在执行效率上并不是很高,随着用户体验的日益重视,前端性能对用户体验的影响备受关注,但由于性能问题相对复杂,接下来我们来了解下JavaScript如何提高性能;

从加载上优化:合理放置脚本位置

由于 JavaScript 的阻塞特性,在每一个<script>出现的时候,无论是内嵌还是外链的方式,它都会让页面等待脚本的加载解析和执行,

并且<script>标签可以放在页面的<head>或者<body>中,因此,如果我们页面中的 css 和 js 的引用顺序或者位置不一样,即使是同样

的代码,加载体验都是不一样的。示例如下:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <title>js 引用的位置性能优化</title>
        <script type="text/javascript" src="index-1.js"></script>
        <script type="text/javascript" src="index-2.js"></script>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
        <div id="app"></div>
    </body>
</html>

以上代码是一个简单的 html 界面,其中加载了两个 js 脚本文件和一个 css 样式文件,由于 js 的阻塞问题,当加载到 index-1.js 的时候,

其后面的内容将会被挂起等待,直到index-1.js 加载、执行完毕,才会执行第二个脚本文件 index-2.js,这个时候页面又将被挂起等待脚

本的加载和执行完成,一次类推,这样用户打开该界面的时候,界面内容会明显被延迟,我们就会看到一个空白的页面闪过,这种体验是

明显不好的,因此 我们应该尽量的让内容和样式先展示出来,将 js 文件放在 最后,以此来优化用户体验。如下所示:

<!DOCTYPE html>
<html>

    <head>
     <meta charset="utf-8">
        <meta name="viewport" content="width=device-width,initial-scale=1.0">
        <title>js 引用的位置性能优化</title>
        <link rel="stylesheet" href="style.css">
    </head>
    <body>
     <div id="app"></div>
     <script type="text/javascript" src="index-1.js"></script>
     <script type="text/javascript" src="index-2.js"></script>
    </body>
</html>

这段代码展示了在 HTML 文档中放置<script>标签的推荐位置。尽管脚本下载会阻塞另一个脚本,但是页面的大部分内容都已经下载完

成并显示给了用户,因此页面下载不会显得太慢。这是雅虎特别性能小组提出的优化 JavaScript 的首要规则:将脚本放在底部。

从请求次数上优化:减少请求次数

由于每个<script>标签初始下载时都会阻塞页面渲染,所以减少页面包含的<script>标签数量有助于改善这一情况。这不仅针对外链脚本,内嵌脚本的数量同样也要限制。浏览器在解析 HTML 页面的过程中每遇到一个<script>标签,都会因执行脚本而导致一定的延时,因此最小化延迟时间将会明显改善页面的总体性能。

这个问题在处理外链 JavaScript 文件时略有不同。考虑到 HTTP 请求会带来额外的性能开销,因此下载单个 100Kb 的文件将比下载 5 个 20Kb 的文件更快。也就是说,减少页面中外链脚本的数量将会改善性能。

通常一个大型网站或应用需要依赖数个 JavaScript 文件。您可以把多个文件合并成一个,这样只需要引用一个<script>标签,就可以减少性能消耗。文件合并的工作可通过离线的打包工具或者一些实时的在线服务来实现。

需要特别提醒的是,把一段内嵌脚本放在引用外链样式表的之后会导致页面阻塞去等待样式表的下载。这样做是为了确保内嵌脚本在执行时能获得最精确的样式信息。因此,建议不要把内嵌脚本紧跟在标签后面。

有一点我们需要知道:页面加载的过程中,最耗时间的不是 js 本身的加载和执行,相比之下,每一次去后端获取资源,客户端与后台建立链接才是最耗时的,也就是大名鼎鼎的Http 三次握手,当然,http 请求不是我们这一次讨论的主题,因此,减少 HTTP 请求,是我们着重优化的一项,事实上,在页面中 js 脚本文件加载很很多情况下,它的优化效果是很显著的。

从加载方式上优化:无阻塞脚本加载

在 JavaScript 性能优化上,减少脚本文件大小并限制 HTTP 请求的次数仅仅是让界面响应 迅速的第一步,现在的 web 应用功能丰富,js 脚本越来越多,光靠精简源码大小和减少 次数不总是可行的,即使是一次 HTTP 请求,但文件过于庞大,界面也会被锁死很长一段 时间,这明显不好的,因此,无阻塞加载技术应运而生。简单来说, 就是 页面在加载完成后才加载 s js 代码,也就是在 w window 对象的 d load 事件触 发后才去下载脚本。要实现这种方式,常用以下几种方式:

延迟脚本加载( defer )

HTML4 为<script>标签定义了一个扩展属性:defer。Defer 属性指明本元素所含的脚本不会修改 DOM,因此代码能安全地延迟执行。defer 属性只被 IE 4 和 Firefox 3.5 更高版本的浏览器所支持,所以它不是一个理想的跨浏览器解决方案。在其他浏览器中,defer 属性会被直接忽略,因此<script>标签会以默认的方式处理,也就是说会造成阻塞。然而,如果您的目标浏览器支持的话,这仍然是个有用的解决方案。

<script type="text/javascript" src="index-1.js" defer></script>

带有 defer 属性的<script>标签可以放置在文档的任何位置。对应的 JavaScript 文件将在页面解析到<script>标签时开始下载,但不会执行,直到 DOM 加载完成,即 onload事件触发前才会被执行。当一个带有 defer 属性的 JavaScript 文件下载时,它不会阻塞浏览的其他进程,因此这类文件可以与其他资源文件一起并行下载。·任何带有 defer 属性的<script>元素在 DOM 完成加载之前都不会被执行,无论内嵌或者是外链脚本都是如此。

延迟脚本加载( async )

HTML5 规范中也引入了 async 属性,用于异步加载脚本,其大致作用和 defer 是一样的,都是采用的并行下载,下载过程中不会有阻塞,但 不同点在于他们的执行时机,c async 需要加载完成后就会自动执行代码 ,但是 r defer 需要等待页面加载完成后才会执行。

从加载方式上优化:动态添加脚本元素

把代码以动态的方式添加的好处是:无论这段脚本是在何时启动下载,它的下载和执行过程都不会阻塞页面的其他进程,我们甚至可以直接添加带头部 head 标签中,都不会影响其他部分。因此,作为开发的你肯定见到过诸如此类的代码块:

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'file.js';
document.getElementsByTagName('head')[0].appendChild(script);

这种方式便是动态创建脚本的方式,也就是我们现在所说的动态脚本创建。通过这种方式下载文件后,代码就会自动执行。但是在现代浏览器中,这段脚本会等待所有动态节点加载完成后再执行。这种情况下,为了确保当前代码中包含的别的代码的接口或者方法能够被成功调用,就必须在别的代码加载前完成这段代码的准备。解决的具体操作思路是:现代浏览器会在 script 标签内容下载完成后接收一个load 事件,我们就可以在 load 事件后再去执行我们想要执行的代码加载和运行,在 IE 中,它会接收 loaded 和 complete事件,理论上是 loaded 完成后才会有 completed,但实践告诉我们他两似乎并没有个先后,甚至有时候只会拿到其中的一个事件,我们可以单独的封装一个专门的函数来体现这个功能的实践性,因此一个统一的写法是:

function LoadScript(url, callback) {
    var script = document.createElement('script');
    script.type = 'text/javascript';
    // IE 浏览器下
    if (script.readyState) {
        script.onreadystatechange = function () {
            if (script.readyState == 'loaded' || script.readyState ==
                'complete') {
                // 确保执行两次
                script.onreadystatechange = null;
                // todo 执行要执行的代码
                callback()
            }
        }
    } else {
        script.onload = function () {
            callback();
        }
    }
    script.src = 'file.js';
    document.getElementsByTagName('head')[0].appendChild(script);
}

LoadScript 函数接收两个参数,分别是要加载的脚本路径和加载成功后需要执行的回调函数,LoadScript 函数本身具有特征检测功能,根据检测结果(IE 和其他浏览器),来决定脚本处理过程中监听哪一个事件。实际上这里的 LoadScript()函数,就是我们所说的 LazyLoad.js(懒加载)的原型。

从加载方式上优化:XMLHttpRequest 脚本注入

通过 XMLHttpRequest 对象来获取脚本并注入到页面也是实现无阻塞加载的另一种方式,这个我觉得不难理解,这其实和动态添加脚本的方式是一样的思想,来看具体代码:

var xhr = new XMLHttpRequest();
xhr.open('get', 'file-1.js', true);
xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) {
        if (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) {
            // 如果从后台或者缓存中拿到数据,则添加到 script 中并加载执行。
            var script = document.createElement('script');
            script.type = 'text/javascript';
            script.text = xhr.responseText;
            // 将创建的 script 添加到文档页面
            document.body.appendChild(script);
        }
    }
}

通过这种方式拿到的数据有两个优点:其一,我们可以控制脚本是否要立即执行,因为我们知道新创建的 script 标签只要添加到文档界面中它就会立即执行,因此,在添加到文档界面之前,也就是在 appendChild()之前,我们可以根据自己实际的业务逻辑去实现需求,到想要让它执行的时候,再 appendChild()即可。其二:它的兼容性很好,所有主流浏览器都支持,它不需要想动态添加脚本的方式那样,我们自己去写特性检测代码;但由于是使用了 XHR 对象,所以不足之处是获取这种资源有“域”的限制。资源 必须在同一个域下才可以,不可以跨域操作。

总结

减少 JavaScript 对性能的影响有以下几种方法:

  • 将所有的<script>标签放到页面底部,也就是</body>闭合标签之前,这能确保在 脚本执行前页面已经完成了渲染。
  • 尽可能地合并脚本。页面中的<script>标签越少,加载也就越快,响应也越迅速。无论是外链脚本还是内嵌脚本都是如此。
  • 采用无阻塞下载 JavaScript 脚本的方法:
    • 使用<script>标签的 defer 属性(仅适用于 IE 和 Firefox 3.5 以上版 本);
    • 使用动态创建的<script>元素来下载并执行代码;
    • 使用 XHR 对象下载 JavaScript 代码并注入页面中。

通过以上策略,可以在很大程度上提高那些需要使用大量 JavaScript 的 Web 网站和应用的实际性能。

小伙伴们,快快用实践一下吧!如果在学习过程中,有遇到任何问题,欢迎加我好友,我拉你进Python学习交流群共同探讨学习。

手把手教你使用JavaScript打造一款扫雷游戏

大家好,我是皮皮。

扫雷大家都玩过,今天我们就是用JavaScript来打造扫雷游戏。废话不多说,直接看下效果;

上图是失败后的结果。

一、思路分析

我们新建一个首页,在首页放置一个点击开始游戏的按钮,动态生成100个小格,即100div;然后通过点击div进行扫雷操作,然后扫雷成功或者失败显示对应的结果;

二、静态页面搭建

2.1 结构层

<body>
    <div class="wrapper">
        <div class="btn" id="btn"></div>  <!-- 开始游戏按钮-->
        <div class="box" id="box"></div>  <!-- 存放小雷的div-->
        <div class="flagBox" id="flagBox"> <!-- 游戏结束才显示的当前雷数的div-->
            当前剩余雷数:
            <span id="score">10</span>
        </div>
        <div class="alertBox" id="alertBox">  <!-- Game over弹出的框(窗口)-->
            <div class="alertImg" id="alertImg">
                <div class="close" id="close"></div>
            </div>
        </div>
    </div>
</body>

2.2 样式层

清楚默认边距

*{
    margin:0;
    padding:0;
}

页面最大div

.wrapper {
    width:100%;
    height:1000px;
    position: fixed;
    top:0;
    left:0;
    background-image: url('img/bg.jpg');
    background-size: 100% 100%;
}

效果如下:

开始游戏按钮

.btn{
    height:140px;
    width:170px;
    position:absolute;
    left:50px;
    background-image: url('img/startGame.png');
    background-size: 100% 100%;
    cursor: pointer;
}

储存雷的大div

.box{
    height:500px;
    width:500px;
    transform: perspective(800px) rotateX(45deg);
    margin:20px auto;
    border-top:1px solid #B25F27;
    border-left:1px solid #B25F27;
    box-shadow:  5px 5px 5px rgba(0,0,0,0.3);
    display:none; /* 先设置为none,开始游戏后显示block */ 
}

每一个方块的小div(一共100个)

.block{
    width:49px;
    height:49px;
    border-right:1px solid #B25F27;
    border-bottom:1px solid #B25F27;
    box-shadow: 0 0 4px #333 inset;
    background-image: url('img/cao.jpg');
    float: left;
}

当前所剩雷数

.flagBox{
    position:absolute;
    top:50px;
    left:50%;
    width:200px;
    height:50px;
    margin-left:-100px;
    color:#333;
    font-size:20px;
    font-weight: bolder;
    display:none; /* 先设置为none,开始游戏后显示block */ 
}

Game Over

.alertBox{
    display:none; /* 先设置为none,开始结束显示block */ 
    position:absolute;
    width:100%;
    height:100%;
    left:0;
    top:0;
    background-color: rgba(0,0,0,0.2);
}

游戏结束弹出窗口右上角的X

.close{
    position:absolute;
    right:0;
    top:0;
    height:40px;
    width:40px;
    background-image: url('img/closeBtn.png');
    background-size: 100% 100%;
    cursor: pointer;

}

三、js页面交互

3.1 获取元素及变量初始化

var startBtn = document.getElementById('btn');
var box = document.getElementById('box');
var flagBox = document.getElementById('flagBox');
var alertBox = document.getElementById('alertBox');
var alertImg = document.getElementById('alertImg');
var closeBtn = document.getElementById('close');
var score = document.getElementById('score');
// 先声明变量,但是不初始化
var minesNum;
var mineOver;
var block;
var mineMap = [];
var startGameBool = true;

3.2 10个雷的初始化设置

function init() {
    minesNum = 10;
    mineOver = 10;
    score.innerHTML = mineOver;
    
    for (var i = 0; i < 10; i++) { // 双层循环 10 * 10 个div
        for (var j = 0; j < 10; j++) {
            var con = document.createElement('div');
            con.classList.add('block'); // 给创建出来的div添加类名 block 
            con.setAttribute('id', i + '-' + j);
            box.appendChild(con);
            mineMap.push({ mine: 0 });
        }
    }
    block = document.getElementsByClassName('block');
    while (minesNum) { // 创建一个10次的循环,即设置10个雷
        var mineIndex = Math.floor(Math.random() * 100);
        if (mineMap[mineIndex].mine === 0) {
            mineMap[mineIndex].mine = 1;
            block[mineIndex].classList.add('isLei'); // 10个雷有小div的block类属性,还有自己的属性,isLei
            minesNum--;
        }
    }
}

3.3 游戏开始事件封装

function bindEvent() {
    startBtn.onclick = function () { // 开始按钮点击事件
        if(startGameBool){
            box.style.display = 'block';
            flagBox.style.display = 'block';
            init();
            startGameBool = false;
        }
    }
    box.oncontextmenu = function () {
        return false;
    }
    box.onmousedown = function (e) { // 小div鼠标按下事件封装
        var event = e.target;
        if (e.which == 1) { //Netscape/Firefox/Opera中不支持 window.event.keyCode,需要用event.which代替
            leftClick(event);
        } else if (e.which == 3) {
            rightClick(event);
        }
    }
    closeBtn.onclick = function () { // 游戏结束,弹出game over窗口的关闭按钮事件封装
        alertBox.style.display = 'none';
        flagBox.style.display = 'none';
        box.style.display = 'none';
        box.innerHTML = '';
        startGameBool = true;
    }
}

3.4 核心事件函数封装

leftClick 没有雷 –> 显示数字(代表以当前小格为中心周围8个格的雷数)扩散(当前周围八个格没有雷) 有雷 –> game Over

function leftClick(dom) {
    if(dom.classList.contains('flag')){
        return;
    }
    var isLei = document.getElementsByClassName('isLei'); // 获得前面的10个雷的div
    if (dom && dom.classList.contains('isLei')) { // 判断是不是雷块
        for (var i = 0; i < isLei.length; i++) {
            isLei[i].classList.add('show'); // 显示地雷背景图
        }
        setTimeout(function () {
            alertBox.style.display = 'block';
            alertImg.style.backgroundImage = 'url("img/over.jpg")';  // 上面显示雷,标志游戏结束
        }, 800)
    } else { // 否则继续扫雷
        var n = 0;
        var posArr = dom && dom.getAttribute('id').split('-');
        var posX = posArr && +posArr[0];
        var posY = posArr && +posArr[1];
        dom && dom.classList.add('num');
        for (var i = posX - 1; i <= posX + 1; i++) {
            for (var j = posY - 1; j <= posY + 1; j++) {
                var aroundBox = document.getElementById(i + '-' + j);
                if (aroundBox && aroundBox.classList.contains('isLei')) {
                    n++;
                }
            }
        }
        dom && (dom.innerHTML = n);
        if (n == 0) {
            for (var i = posX - 1; i <= posX + 1; i++) {
                for (var j = posY - 1; j <= posY + 1; j++) {
                    var nearBox = document.getElementById(i + '-' + j);
                    if (nearBox && nearBox.length != 0) {
                        if (!nearBox.classList.contains('check')) {
                            nearBox.classList.add('check');
                            leftClick(nearBox);
                        }
                    }
                }
            }
        }
    }
}

rightClick 没有标记并且没有数字 –> 进行标记;

有标记 –> 取消标记 –> 标记是否正确,10个都正确标记,提示成功;

如果已经出现,则点击无效果;

function rightClick(dom){
    if(dom.classList.contains('num')){ // 如果已经出现,则点击无效果
        return;
    }
    dom.classList.toggle('flag'); // 在元素中切换类名,切换为flag类名,显示红旗背景图;此处的雷被扫除了
    if(dom.classList.contains('isLei') && dom.classList.contains('flag')){
        mineOver --; // 雷数减一
    }
    if(dom.classList.contains('isLei') && !dom.classList.contains('flag')){
        mineOver ++;
    }

    score.innerHTML = mineOver;
    if(mineOver == 0){ // 扫完雷,标志雷数量为0
        alertBox.style.display = 'block';
        alertImg.style.backgroundImage = 'url("img/success.png")'; // 游戏胜利
    }
}

3.5 游戏开始

bindEvent()

四、总结

本文我们通过JavaScript打造了简单的扫雷游戏,首先是设计下简单的界面样式,然后通过扫雷的逻辑动态构建雷块的位置,通过点击小方块进行扫雷,感兴趣的小伙伴可以去试一下。

小伙伴们,快快用实践一下吧!如果在学习过程中,有遇到任何问题,欢迎加我好友,我拉你进Python学习交流群共同探讨学习。

手把手教会你JavaScript引擎如何执行JavaScript代码

JavaScript 在运行过程中与其他语言有所不一样,如果不理解 JavaScript 的词法环境、执行上下文等内容,很容易会在开发过程中产生 Bug,比如this指向和预期不一致、某个变量不知道为什么被改了,等等。所以今天我们就来聊一聊 JavaScript 代码的运行过程。

大家都知道,JavaScript 代码是需要在 JavaScript 引擎中运行的。我们在说到 JavaScript 运行的时候,常常会提到执行环境、词法环境、作用域、执行上下文、闭包等内容。这些概念看起来都差不多,却好像又不大容易区分清楚,它们分别都在描述什么呢?

这些词语都是与 JavaScript 引擎执行代码的过程有关,为了搞清楚这些概念之间的区别,我们可以回顾下 JavaScript 代码运行过程中的各个阶段。

JavaScript 代码运行的各个阶段

JavaScript 是弱类型语言,在运行时才能确定变量类型。JavaScript 引擎在执行 JavaScript 代码时,也会从上到下进行词法分析、语法分析、语义分析等处理,并在代码解析完成后生成 AST(抽象语法树),最终根据 AST 生成 CPU 可以执行的机器码并执行。

这个过程,我们称之为语法分析阶段。除了语法分析阶段,JavaScript 引擎在执行代码时还会进行其他的处理。以 V8 引擎为例,在 V8 引擎中 JavaScript 代码的运行过程主要分成三个阶段。

  1. 语法分析阶段。该阶段会对代码进行语法分析,检查是否有语法错误(SyntaxError),如果发现语法错误,会在控制台抛出异常并终止执行。
  1. 编译阶段。该阶段会进行执行上下文(Execution Context)的创建,包括创建变量对象、建立作用域链、确定 this 的指向等。每进入一个不同的运行环境时,V8 引擎都会创建一个新的执行上下文。
  2. 执行阶段。将编译阶段中创建的执行上下文压入调用栈,并成为正在运行的执行上下文,代码执行结束后,将其弹出调用栈。

其中,语法分析阶段属于编译器通用内容,就不再赘述。前面提到的执行环境、词法环境、作用域、执行上下文等内容都是在编译和执行阶段中产生的概念。

执行上下文的创建

执行上下文的创建离不开 JavaScript 的运行环境,JavaScript 运行环境包括全局环境、函数环境和eval,其中全局环境和函数环境的创建过程如下:

  1. 第一次载入 JavaScript 代码时,首先会创建一个全局环境。全局环境位于最外层,直到应用程序退出后(例如关闭浏览器和网页)才会被销毁。
  2. 每个函数都有自己的运行环境,当函数被调用时,则会进入该函数的运行环境。当该环境中的代码被全部执行完毕后,该环境会被销毁。不同的函数运行环境不一样,即使是同一个函数,在被多次调用时也会创建多个不同的函数环境。

在不同的运行环境中,变量和函数可访问的其他数据范围不同,环境的行为(比如创建和销毁)也有所区别。而每进入一个不同的运行环境时,JavaScript 都会创建一个新的执行上下文,该过程包括:

  • 建立作用域链(Scope Chain);
  • 创建变量对象(Variable Object,简称 VO);
  • 确定 this 的指向。

由于建立作用域链过程中会涉及变量对象的概念,因此我们先来看看变量对象的创建,再看建立作用域链和确定 this 的指向。

创建变量对象

变量对象(VO)

每个执行上下文都会有一个关联的变量对象,该对象上会保存这个上下文中定义的所有变量和函数。

在浏览器中,全局环境的变量对象是window对象,因此所有的全局变量和函数都是作为window对象的属性和方法创建的。相应的,在 Node 中全局环境的变量对象则是global对象。

创建VO的过程

创建变量对象将会创建arguments对象(仅函数环境下),同时会检查当前上下文的函数声明和变量声明。

  • 对于变量声明:此时会给变量分配内存,并将其初始化为undefined(该过程只进行定义声明,执行阶段才执行赋值语句)。
  • 对于函数声明:此时会在内存里创建函数对象,并且直接初始化为该函数对象。

变量声明和函数声明的处理过程,便是我们常说的变量提升和函数提升,其中函数声明提升会优先于变量声明提升。因为变量提升容易带来变量在预期外被覆盖掉的问题,同时还可能导致本应该被销毁的变量没有被销毁等情况。因此 ES6 中引入了let和const关键字,从而使 JavaScript 也拥有了块级作用域。

作用域

在各类编程语言中,作用域分为静态作用域和动态作用域。JavaScript 采用的是词法作用域(Lexical Scoping),也就是静态作用域。词法作用域中的变量,在编译过程中会产生一个确定的作用域。

词法作用域中的变量,在编译过程中会产生一个确定的作用域,这个作用域即当前的执行上下文,在 ES5 后我们使用词法环境(Lexical Environment)替代作用域来描述该执行上下文。因此,词法环境可理解为我们常说的作用域,同样也指当前的执行上下文(注意,是当前的执行上下文)。

在 JavaScript 中,词法环境又分为词法环境(Lexical Environment)和变量环境(Variable Environment)两种,其中:

  • 变量环境用来记录var/function等变量声明;
  • 词法环境是用来记录let/const/class等变量声明。

也就是说,创建变量过程中会进行函数提升和变量提升,JavaScript 会通过词法环境来记录函数和变量声明。通过使用两个词法环境(而不是一个)分别记录不同的变量声明内容,JavaScript 实现了支持块级作用域的同时,不影响原有的变量声明和函数声明。

这就是创建变量的过程,它属于执行上下文创建中的一环。创建变量的过程会产生作用域,作用域也被称为词法环境。

建立作用域链

作用域链,就是将各个作用域通过某种方式连接在一起。作用域就是词法环境,而词法环境由两个成员组成。

  1. 环境记录(Environment Record):用于记录自身词法环境中的变量对象。
  2. 外部词法环境引用(Outer Lexical Environment):记录外层词法环境的引用。

通过外部词法环境的引用,作用域可以层层拓展,建立起从里到外延伸的一条作用域链。当某个变量无法在自身词法环境记录中找到时,可以根据外部词法环境引用向外层进行寻找,直到最外层的词法环境中外部词法环境引用为null,这便是作用域链的变量查询。

JavaScript 代码运行过程分为定义期和执行期,前面提到的编译阶段则属于定义期,代码示例如下:

function foo() { // 定义全局函数foo
    console.dir(bar);
    var a = 1;
    function bar() { // 在foo函数内部定义函数bar
        a = 2;
 }
}
console.dir(foo);
foo();

前面我们说到,JavaScript 使用的是静态作用域,因此函数的作用域在定义期已经决定了。在上面的例子中,全局函数foo创建了一个foo[[scope]]属性,包含了全局[[scope]]

foo[[scope]] = [globalContext];

而当我们执行foo()时,也会分别进入foo函数的定义期和执行期。

在foo函数的定义期时,函数bar的[[scope]]将会包含全局[[scope]]和foo的[[scope]]:

bar[[scope]] = [fooContext, globalContext];

运行上述代码,我们可以在控制台看到符合预期的输出:

可以看到:

  • foo的[[scope]]属性包含了全局[[scope]]
  • bar的[[scope]]将会包含全局[[scope]]和foo的[[scope]]

也就是说,JavaScript 会通过外部词法环境引用来创建变量对象的一个作用域链,从而保证对执行环境有权访问的变量和函数的有序访问。除了创建作用域链之外,在这个过程中还会对创建的变量对象做一些处理。

在编译阶段会进行变量对象(VO)的创建,该过程会进行函数声明和变量声明,这时候变量的值被初始化为 undefined。在代码进入执行阶段之后,JavaScript 会对变量进行赋值,此时变量对象会转为活动对象(Active Object,简称 AO),转换后的活动对象才可被访问,这就是 VO -> AO 的过程,示例如下:

function foo(a) {
    var b = 2; 
    function c() {}
    var d = function() {};
}


foo(1);

在执行foo(1)时,首先进入定义期,此时:

  • 参数变量a的值为1
  • 变量b和d初始化为undefined
  • 函数c创建函数并初始化
AO = {
 arguments: {
  0: 1,
  length: 1
 },
 a: 1,
 b: undefined,
 c: reference to function() c() {}
 d:undefined
}

前面我们也有提到,进入执行期之后,会执行赋值语句进行赋值,此时变量b和d会被赋值为 2 和函数表达式:

AO = {
   arguments: {
    0: 1,
    length: 1
  },
  a: 1,
  b: 2,
  c: reference to function c(){},
  d: reference to FunctionExpression "d"
}

这就是 VO -> AO 过程。

  • 在定义期(编译阶段):该对象值仍为undefined,且处于不可访问的状态。
  • 进入执行期(执行阶段):VO 被激活,其中变量属性会进行赋值。

实际上在执行的时候,除了 VO 被激活,活动对象还会添加函数执行时传入的参数和arguments这个特殊对象,因此 AO 和 VO 的关系可以用以下关系来表达:

AO = VO + function parameters + arguments

现在,我们知道作用域链是在进入代码的执行阶段时,通过外部词法环境引用来创建的。总结如下:

  • 在编译阶段,JavaScript 在创建执行上下文的时候会先创建变量对象(VO);
  • 在执行阶段,变量对象(VO)被激活为活动对象( AO),函数内部的变量对象通过外部词法环境的引用创建作用域链。

通过作用域链,我们可以在函数内部可以直接读取外部以及全局变量,但外部环境是无法访问内部函数里的变量。示例如下:

function foo() {
  var a = 1;
}
foo();
console.log(a); // undefined

我们在全局环境下无法访问函数foo中的变量a,这是因为全局函数的作用域链里,不含有函数foo内的作用域。

如果我们想要访问内部函数的变量,可以通过函数foo中的函数bar返回变量a,并将函数bar返回,这样我们在全局环境中也可以通过调用函数foo返回的函数bar,来访问变量a:

function foo() {
  var a = 1;
  function bar() {
    return a;
  }
  return bar;
}
var b = foo();
console.log(b()); // 1

当函数执行结束之后,执行期上下文将被销毁,其中包括作用域链和激活对象。

在上面的实例中;当b()执行时,foo函数上下文包括作用域都已经被销毁了,但是foo作用域下的a依然可以被访问到;这是因为bar函数引用了foo函数变量对象中的值,此时即使创建bar函数的foo函数执行上下文被销毁了,但它的变量对象依然会保留在 JavaScript 内存中,bar函数依然可以通过bar函数的作用域链找到它,并进行访问。这就是闭包;

闭包使得我们可以从外部读取局部变量,常见的用途包括:

  1. 用于从外部读取其他函数内部变量的函数;
  2. 可以使用闭包来模拟私有方法;
  3. 让这些变量的值始终保持在内存中。

注意,在使用闭包的时候,需要及时清理不再使用到的变量,否则可能导致内存泄漏问题。

确定 this 的指向

在 JavaScript 中,this指向执行当前代码对象的所有者,可简单理解为this指向最后调用当前代码的那个对象。

根据 JavaScript 中函数的调用方式不同,this的指向分为以下情况。

  1. 在全局环境中,this指向全局对象(在浏览器中为window)
  2. 在函数内部,this的值取决于函数被调用的方式
  3. 函数作为对象的方法被调用,this指向调用这个方法的对象
  4. 函数用作构造函数时(使用new关键字),它的this被绑定到正在构造的新对象
  5. 在类的构造函数中,this是一个常规对象,类中所有非静态的方法都会被添加到this的原型中
  6. 在箭头函数中,this指向它被创建时的环境
  7. 使用apply、call、bind等方式调用:根据 API 不同,可切换函数执行的上下文环境,即this绑定的对象

可以看到,this在不同的情况下会有不同的指向,在 ES6 箭头函数还没出现之前,为了能正确获取某个运行环境下this对象,我们常常会使用以下代码:

var that = this;
var self = this;

这样的代码将变量分配给this,便于使用。但是降低了代码可读性,不推荐使用,通过正确使用箭头函数,我们可以更好地管理作用域。

总结

今天我们了解了 JavaScript 代码的运行过程,该过程分为语法分析阶段、编译阶段、执行阶段三个阶段。

在编译阶段,JavaScript会进行执行上下文的创建,在执行阶段,变量对象(VO)会被激活为活动对象(AO),变量会进行赋值,此时活动对象才可被访问。在执行结束之后,作用域链和活动对象均被销毁,使用闭包可使活动对象依然被保留在内存中。这就是 JavaScript 代码的运行过程。

基于HTML5打造的一款别踩白板小游戏

背景简介

别踩白板这个游戏相信大家都玩过,这个是基于HTML5打造的简单小游戏,在PC端和移动端都能够运行,适应多种平台,今天我们使用原生JS搭配JQuery构建这个小游戏–别踩白板。

一、思路分析

整体页面是一个大的矩形,长宽比例大概是3:2,然后游戏开始,不断有白板降落,然后一行是4个板,一块黑色板块,其余三块是白色板块,通过板块的点击事件绑定,然后判定是什么颜色,只要是白色的,游戏结束(Game Over), 否则玩家积分加1;

二、页面搭建

2.1 HTML层

<div class="wrapper">
        <div id="go">
            <a href="javaScript:void(0)" id="go">Game Start</a>
        </div>
        <div id="main"></div>
</div>

2.2 CSS层

在样式设置前还是先了解大致结构,如下所示:

全局设置样式

*{
    margin:0;
    padding:0;
}

wrapper样式设置样式

.wrapper{
    margin:150px auto;
    width:400px;
    height:600px;
    border:1px solid #ccc;
    position: relative;
    overflow: hidden;
}

wrapper下面的go的设置样式

#go{
    width:100%;
    position: absolute;
    top:0;
    text-align: center;
    z-index:99;
}

开始游戏按钮设置样式

#go a{
    display:block;
    height:100px;
    width:400px;
    color:cyan;
    background-color: #000000;
    text-decoration: none;
    border-bottom:3px dashed #eee;
    font-size:60px;
    font-weight:600;
}

main(方块)设置样式

#main{
    width:400px;
    height:600px;
    position: relative;
    top:-150px;
    /* border:1px solid black; */
}

创建出来的每一行方块设置样式

.row{
    width:400px;
    height:150px;
}

一行中的四个小方块的设置样式

.row div{
   width:99px;
   height:149px;
   border-left:1px solid #222;
   border-bottom:1px solid #222;
   float: left;
   cursor: pointer;
}

在设置好样式之后,得到的大致界面如下所示:

可以看到界面样式比较简单,我们的想法是点击Start Game 按钮后方块自动降落,所以屏幕比较空(暂时)。

2.3 JS层

js层主要用来控制页面产生动态效果;比如产生方块,以及方块的移动等等;

2.3.1获取元素

var main = document.getElementById('main'); // 获取dom元素
var go = document.getElementById('go'); 
var speed = 5, num = 0, timer, flag = true; // 设置初始变量
var colors = ['red', 'green', 'black', 'blue']; // 设置存放颜色的数组

这里存放颜色的数组不需要白色,每一个初始化出来的方块不设置背景颜色,它默认是白色;

2.3.1创建每一行div元素

前面我们说过,一行是四个方块,比例和大方块一样(3:2),它的长宽是:{width: 100px ; height: 150px};

function cDiv() {
    var oDiv = document.createElement('div');// 获取一个随机数将每一行找到一个随机div 设置上颜色
    var index = Math.floor(Math.random() * 4);
    
    oDiv.setAttribute('class', 'row'); // 设置行class类名
    for (var j = 0; j < 4; j++) { // for循环生成一行四个div
        var iDiv = document.createElement('div');
        oDiv.appendChild(iDiv); // 将每一个小div插入每一行中
    }
    
    if (main.childNodes.length == 0) { // 根据父级中是否有子元素   插入新生成的行
        main.appendChild(oDiv);  // 如果父级为空  直接插入
    } else {
        main.insertBefore(oDiv, main.childNodes[0]); // 如果父级有元素   将新生成的一行插入到已有行数的最前面
    } 
    var clickDiv = main.childNodes[0].childNodes[index]; // 根据随机数 设置一行中有颜色的div
    clickDiv.setAttribute('class', 'i'); // 将此元素设置class类名  作为需要点击的标记
    clickDiv.style.backgroundColor = colors[index]; // 同时设置上背景颜色
}

2.3.2点击事件函数封装

function bindEvent() {
    main.addEventListener('click', function (event) { // 给main添加点击事件
        if (flag) {  // 根据flag值判断是否可以点击
            var tar = event.target;  // 获得到点击的源事件
            if (tar.className == 'i') { // 判断点击的块是否为有颜色的 
                tar.style.backgroundColor = '#bbb'; // 改变背景颜色 
                tar.classList.remove('i'); // 移除class类名               
                num++;   // 计数++
            } else {                
                alert('游戏结束,得分:' + num); // 如果点到了白色的块   游戏结束
                clearInterval(timer);
                flag = false;
            }            
            if (num % 10 == 0) { // 如果当前分数为10的倍数   速度++
                speed++;
            }
        }
    })
}

2.3.4 方块移动函数封装

function move() {
    clearInterval(timer); 
    timer = setInterval(function () { // 设置定时器
        var step = parseInt(main.offsetTop) + speed; // 利用top值移动main区域
        main.style.top = step + 'px';
        if (parseInt(main.offsetTop) >= 0) { // 如果main区域移动到可视区域 创建一行新的元素
            cDiv();
            main.style.top = '-150px'; // 同时将main区域移动到可视区域上方
        }
        var len = main.childNodes.length; // 获得mian区域内的行数
        if (len == 6) {  // 如果main区域内行数为6   即显示区域四行  上面新生成一行  下面一行
            
            for (var i = 0; i < 4; i++) { // 遍历最后一行的每一个div
                
                if (main.childNodes[len - 1].children[i].classList.contains('i')) { // 如果其中有一个包含没有被点击的   游戏结束
                    alert('游戏结束,得分:' + num);
                    clearInterval(timer);
                    flag = false; // 游戏结束后不可以继续点击
                }
            }
            
            main.removeChild(main.childNodes[len - 1]); // 将展示过后的每一行移除
        }
    }, 20)
    bindEvent(); // 点击事件
}

在函数里面第一句就是clearInterval(timer);防止定时器多开;

2.3.5 游戏开始

// 开始按钮点击   开始移动 创建每一行元素
function clickStart() {
    go.addEventListener('click', function () {
        go.style.display = 'none';
        move();
    });
}
clickStart();

大致效果如图所示:

这个是使用到HbuilderX中的内置浏览器的界面,游戏结束效果如上图所示;

三、总结

本文我们使用到原生js打造了简单触屏游戏——别踩白板,对于游戏有简单的改动。总体来说首先我们需要设置好游戏界面大致结构和样式,然后通过原生js控制方块的产生移动,以及点击等等,最终才呈现了一个合适的完整的界面效果;感兴趣的小伙伴可以去试一下。

盘点JavaScript中的事件及事件的三种模型

大家好,我是皮皮。

前言

我们知道在很多编程语言都有事件这个概念,在JavaScript中同样存在事件,原因也很简单,我们知道HTML是页面结构层,相当于人的骨架;

CSS是样式层,相当于人的外形;但是它是静态的,一个人应该能动,动起来,所以产生了JavaScript;JavaScript就是用来控制页面元素,与用户产

生动态交互效果,才构成了如今这丰富多样化的界面。今天我们就来认识一下js当中的事件;

一、事件相关概念

事件:指用户的鼠标动作和键盘动作,document的load和unloaded,事件被封装成一个event对象,包含了该事件发生时的所有相关信息(event的属性)以及可以对事件进行的操作(event的方法)。

  • 比如点击页面上一个按钮,产生的event对象如下:
盘点JavaScript中的事件及事件的三种模型

以上可以看到是一个MouseEvent对象,包含了一系列属性,如鼠标点击的位置等。

  • 敲击键盘时产生的event对象
盘点JavaScript中的事件及事件的三种模型

  • window.onload监听函数中打印出event对象如下:
盘点JavaScript中的事件及事件的三种模型

注意:Event 类是MouseEvent、KeyboardEvent的父类;

二、事件对象常用属性和方法

2.1事件定位相关属性

MouseEvent对象里的属性,很多带X/Y,它们都和事件的位置相关。具体包括:x/y、clientX/clientY、pageX/pageY、screenX/screenY、layerX/layerY、offsetX/offsetY 六对;之所以能有这么多是因为各浏览器厂商在版本更迭的时候产生了很多不一致:

以移动鼠标为例:

x属性

Y属性

功能

x

Y

距浏览器可视区域(工具栏除外区域)左/上的距离;

clientX

clientY

距浏览器可视区域(工具栏除外区域)左/上的距离(同上);

screenX

screenY

距计算机显示器左/上的距离,拖动你的浏览器窗口位置可以看到变化;

offsetX

offsetY

距有定位属性的父元素左/上的距离;(不计算边框)

pageX

pageY

距页面左/上的距离,它与clientX/clientY的区别是不随滚动条的位置变化;

layerX

layerY

距有定位属性的父元素左/上的距离(计算边框);

之所以有那么多值一样的情况,就是由于浏览器兼容的原因, 针对于浏览器对于不同属性的兼容情况如下所示;(+支持,-不支持)

浏览器

offsetX/offsetY

x/y

layerX/layerY

pageX/pageY

clientX/clientY

screenX/screenY

W3C

+

+

IE

+

+

+

+

Firefox

+

+

+

+

Opera

+

+

+

+

+

Safari

+

+

+

+

+

+

chrome

+

+

+

+

+

 说明:详情可查

https://www.caniuse.com/

2.2其他常用属性

  • target:发生事件的节点;
  • currentTarget:当前正在处理的事件的节点,在事件捕获或冒泡阶段;
  • timeStamp:事件发生的时间,时间戳。
  • bubbles:事件是否冒泡。
  • cancelable:事件是否可以用preventDefault()方法来取消默认的动作;
  • keyCode:按下的键的值;

2.3 event对象的方法

  • event. preventDefault(): 阻止元素默认的行为,如链接的跳转、表单的提交;
  • event. stopPropagation(): 阻止事件冒泡;
  • event.initEvent(): 初始化新事件对象的属性,自定义事件会用,不常用;
  • event. stopImmediatePropagation(): 可以阻止掉同一事件的其他优先级较低的侦听器的处理很少使用;

三、事件的三种模型

3.1 原始事件模型(DOM0级)

在原始事件模型中,事件发生后没有传播的概念,没有事件流。事件发生,马上处理。监听函数只是元素的一个属性值,通过指定元素的属性值来绑定监听器。书写方式有两种:

  1. HTML代码中指定属性值:
<input type="button" onclick="func1()" />
  1. 在js代码中指定属性值:
document.getElementsByTagName(‘input’)[0].onclick = func1

优点:所有浏览器都兼容

缺点:

  • 逻辑与显示没有分离;
  • 相同事件的监听函数只能绑定一个,后绑定的会覆盖掉前面的,如:a.onclick = func1; a.onclick = func2;将只会执行func2中的内容;
  • 无法通过事件的冒泡、委托等机制完成更多事情;

3.2 IE事件模型

“IE不把该对象传入事件处理函数,由于在任意时刻只会存在一个事件,所以IE把它作为全局对象window的一个属性”,用IE8执行了代码alert(window.event),结果弹出是null,说明该属性已经定义,只是值为null(与undefined不同),代码如下;

window.onload = function (){alert(window.event);}


setTimeout(function(){alert(window.event);},2000);

第一次弹出【object event】,两秒后弹出依然是null。由此可见IE是将event对象在处理函数中设为window的属性,一旦函数执行结束,便被置为null了;

IE的事件模型只有两步:

  1. 先执行元素的监听函数,
  2. 然后事件沿着父节点一直冒泡到document。

IE模型下的事件监听方式比较独特,绑定监听函数的方法是:

attachEvent( "eventType""handler");//其中evetType为事件的类型,

如onclick,注意要加’on’。解除事件监听器的方法是:

detachEvent("eventType""handler" )

IE的事件模型已经可以解决原始模型的三个缺点,但其自己的缺点就是兼容性,只有IE系列浏览器才可以这样写。

3.2 DOM2事件模型

此模型是W3C制定的标准模型,既然是标准,现代浏览器(指IE6~8除外的浏览器)都已经遵循这个规范。W3C制定的事件模型中,一次事件的发生包含三个过程:

  1. capturing phase:事件捕获阶段。事件被从document一直向下传播到目标元素,在这过程中依次检查经过的节点是否注册了该事件的监听函数,若有则执行;
  2. target phase:事件处理阶段。事件到达目标元素,执行目标元素的事件处理函数;
  3. bubbling phase:事件冒泡阶段。事件从目标元素上升一直到达document,同样依次检查经过的节点是否注册了该事件的监听函数,有则执行;

所有的事件类型都会经历captruing phase但是只有部分事件会经历bubbling phase阶段,例如submit事件就不会被冒泡。

标准的事件监听器绑定

addEventListener("eventType""handler""true|false");

其中eventType指事件类型。第二个参数是处理函数,第三个即用来指定是否在捕获阶段进行处理,一般设为false来与IE保持一致。

监听器的解除也类似:

removeEventListner("eventType""handler""true!false");

以上便是事件的三种模型,我们在开发的时候需要兼顾IE与非IE浏览器,所以注册一个监听器应该这样写:

var a = document.getElementById('a');
if(a.attachEvent){
    a.attachEvent('onclick'func);
}
else{
    a.addEventListener('click'funcfalse);
}

四、总结

本文我们就JavaScript事件做了基本介绍,认识了事件的相关概念,事件的常用属性和方法,以及事件的三种模型,想要完成复杂的界面动态交互效果,事件的使用至关重要,想要深入了解事件的小伙伴可以参考官方手册。

http://www.javascriptcn.com/

HTML页面基本结构和加载过程

大家好,我是皮皮。

前言

对于前端来说,HTML 都是最基础的内容。

今天,我们来了解一下 HTML 和网页有什么关系,以及与 DOM 有什么不同。通过本讲内容,你将掌握浏览器是怎么处理 HTML 内容的,以及在这个过程中我们可以进行怎样的处理来提升网页的性能,从而提升用户的体验。

一、浏览器页面加载过程

不知你是否有过这样的体验:当打开某个浏览器的时候,发现一直在转圈,或者等了好长时间才打开页面……

此时的你,会选择关掉页面还是耐心等待呢?

这一现象,除了网络不稳定、网速过慢等原因,大多数都是由于页面设计不合理导致加载时间过长导致的。

我们都知道,页面是用 HTML/CSS/JavaScript 来编写的。

  • HTML 的职责在于告知浏览器如何组织页面,以及搭建页面的基本结构;
  • CSS 用来装饰 HTML,让我们的页面更好看;
  • JavaScript 则可以丰富页面功能,使静态页面动起来。

HTML由一系列的元素组成,通常称为HTML元素。HTML 元素通常被用来定义一个网页结构,基本上所有网页都是这样的 HTML 结构:

<html>
    <head></head>
    <body></body>
</html>

其中:

  • html元素是页面的根元素,它描述完整的网页;
  • head元素包含了我们想包含在 HTML 页面中,但不希望显示在网页里的内容;
  • body元素包含了我们访问页面时所有显示在页面上的内容,是用户最终能看到的内容;

HTML 中的元素特别多,其中还包括可用于 Web Components 的自定义元素。

前面我们提到页面 HTML 结构不合理可能会导致页面响应慢,这个过程很多时候体现在<script>和<style>元素的设计上,它们会影响页面加载过程中对 Javascript 和 CSS 代码的处理。

因此,如果想要提升页面的加载速度,就需要了解浏览器页面的加载过程是怎样的,从根本上来解决问题。

浏览器在加载页面的时候会用到 GUI 渲染线程和 JavaScript 引擎线程(更详细的浏览器加载和渲染机制将在第 7 讲中介绍)。其中,GUI 渲染线程负责渲染浏览器界面 HTML 元素,JavaScript 引擎线程主要负责处理 JavaScript 脚本程序。

由于 JavaScript 在执行过程中还可能会改动界面结构和样式,因此它们之间被设计为互斥的关系。也就是说,当 JavaScript 引擎执行时,GUI 线程会被挂起。

以网易云课堂官网为例,我们来看看网页加载流程。

(1)当我们打开官网的时候,浏览器会从服务器中获取到 HTML 内容。

(2)浏览器获取到 HTML 内容后,就开始从上到下解析 HTML 的元素。

(3)<head>元素内容会先被解析,此时浏览器还没开始渲染页面。

我们看到<head>元素里有用于描述页面元数据的<meta>元素,还有一些<link>元素涉及外部资源(如图片、CSS 样式等),此时浏览器会去获取这些外部资源。除此之外,我们还能看到<head>元素中还包含着不少的<script>元素,这些<script>元素通过src属性指向外部资源。

(4)当浏览器解析到这里时(步骤 3),会暂停解析并下载 JavaScript 脚本。

(5)当 JavaScript 脚本下载完成后,浏览器的控制权转交给 JavaScript 引擎。当脚本执行完成后,控制权会交回给渲染引擎,渲染引擎继续往下解析 HTML 页面。

(6)此时<body>元素内容开始被解析,浏览器开始渲染页面。

在这个过程中,我们看到<head>中放置的<script>元素会阻塞页面的渲染过程:把 JavaScript 放在<head>里,意味着必须把所有 JavaScript 代码都下载、解析和解释完成后,才能开始渲染页面。

到这里,我们就明白了:如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,用户体验会变得很糟糕。

因此,对于对性能要求较高、需要快速将内容呈现给用户的网页,常常会将 JavaScript 脚本放在<body>的最后面。这样可以避免资源阻塞,页面得以迅速展示。我们还可以使用defer/async/preload等属性来标记<script>标签,来控制 JavaScript 的加载顺序。

百度首页

三、DOM 解析

对于百度这样的搜索引擎来说,必须要在最短的时间内提供到可用的服务给用户,其中就包括搜索框的显示及可交互,除此之外的内容优先级会相对较低。

浏览器在渲染页面的过程需要解析 HTML、CSS 以得到 DOM 树和 CSS 规则树,它们结合后才生成最终的渲染树并渲染。因此,我们还常常将 CSS 放在<head>里,可用来避免浏览器渲染的重复计算。

二、HTML 与 DOM 有什么不同

我们知道<p>是 HTML 元素,但又常常将<p>这样一个元素称为 DOM 节点,那么 HTML 和 DOM 到底有什么不一样呢?

根据 MDN 官方描述:文档对象模型(DOM)是 HTML 和 XML 文档的编程接口。

也就是说,DOM 是用来操作和描述 HTML 文档的接口。如果说浏览器用 HTML 来描述网页的结构并渲染,那么使用 DOM 则可以获取网页的结构并进行操作。一般来说,我们使用 JavaScript 来操作 DOM 接口,从而实现页面的动态变化,以及用户的交互操作。

在开发过程中,常常用对象的方式来描述某一类事物,用特定的结构集合来描述某些事物的集合。DOM 也一样,它将 HTML 文档解析成一个由 DOM 节点以及包含属性和方法的相关对象组成的结构集合。

三、DOM 解析

我们常见的 HTML 元素,在浏览器中会被解析成节点。比如下面这样的 HTML 内容:

<html>
    <head>
        <title>标题</title>
    </head>
    <body>
        <a href='xx.com'>我的超链接</a>
        <h1>页面第一标题</h1>
    </body>
</html>

打开控制台 Elements 面板,可以看到这样的 HTML 结构,如下图所示:

在浏览器中,上面的 HTML 会被解析成这样的 DOM 树,如下图所示:

我们都知道,对于树状结构来说,常常使用parent/child/sibling等方式来描述各个节点之间的关系,对于 DOM 树也不例外。

举个例子,我们常常会对页面功能进行抽象,并封装成组件。但不管怎么进行整理,页面最终依然是基于 DOM 的树状结构,因此组件也是呈树状结构,组件间的关系也同样可以使用parent/child/sibling这样的方式来描述。同时,现在大多数应用程序同样以root为根节点展开,我们进行状态管理、数据管理也常常会呈现出树状结构。

四、事件委托

我们知道,浏览器中各个元素从页面中接收事件的顺序包括事件捕获阶段、目标阶段、事件冒泡阶段。其中,基于事件冒泡机制,我们可以实现将子元素的事件委托给父级元素来进行处理,这便是事件委托。

如果我们在每个元素上都进行监听的话,则需要绑定三个事件;(假设页面上有a,b,c三个兄弟节点)

function clickEventFunction(e) {
  console.log(e.target === this); // logs `true`
  // 这里可以用 this 获取当前元素
}
// 元素a,b,c绑定
element2.addEventListener("click", clickEventFunction, false);
element5.addEventListener("click", clickEventFunction, false);
element8.addEventListener("click", clickEventFunction, false);

使用事件委托,可以通过将事件添加到它们的父节点,而将事件委托给父节点来触发处理函数:

function clickEventFunction(event) {
  console.log(e.target === this); // logs `false`
  // 获取被点击的元素
  const eventTarget = event.target;
  // 检查源元素`event.target`是否符合预期
  // 此处控制广告面板的展示内容
}
// 元素1绑定
element1.addEventListener("click", clickEventFunction, false);

这样能解决什么问题呢?

  • 绑定子元素会绑定很多次的事件,而绑定父元素只需要一次绑定。
  • 将事件委托给父节点,这样我们对子元素的增加和删除、移动等,都不需要重新进行事件绑定。

常见的使用方式主要是上述这种列表结构,每个选项都可以进行编辑、删除、添加标签等功能,而把事件委托给父元素,不管我们新增、删除、更新选项,都不需要手动去绑定和移除事件。

如果在列表数量内容较大的时候,对成千上万节点进行事件监听,也是不小的性能消耗。使用事件委托的方式,我们可以大量减少浏览器对元素的监听,也是在前端性能优化中比较简单和基础的一个做法。

注意:

  1. 如果我们直接在document.body上进行事件委托,可能会带来额外的问题;
  2. 由于浏览器在进行页面渲染的时候会有合成的步骤,合成的过程会先将页面分成不同的合成层,而用户与浏览器进行交互的时候需要接收事件。此时,浏览器会将页面上具有事件处理程序的区域进行标记,被标记的区域会与主线程进行通信。
  3. 如果我们document.body上被绑定了事件,这时候整个页面都会被标记;
  4. 即使我们的页面不关心某些部分的用户交互,合成器线程也必须与主线程进行通信,并在每次事件发生时进行等待。这种情况,我们可以使用passive: true选项来解决

五、总结

我们了解了 HTML 的作用,以及它是如何影响浏览器中页面的加载过程的,同时还介绍了使用 DOM 接口来控制 HTML 的展示和功能逻辑。我们了解了DOM解析事件委托等相关概念。

  1. true选项来解决

五、总结

我们了解了 HTML 的作用,以及它是如何影响浏览器中页面的加载过程的,同时还介绍了使用 DOM 接口来控制 HTML 的展示和功能逻辑。我们了解了DOM解析事件委托等相关概念。

一篇文章告诉你JavaScript 如何实现继承

背景简介

JavaScript 在编程语言界是个特殊种类,它和其他编程语言很不一样,JavaScript 可以在运行的时候动态地改变某个变量的类型。

比如你永远也没法想到像isTimeout这样一个变量可以存在多少种类型,除了布尔值true和false,它还可能是undefined、1和0、一个时间戳,甚至一个对象。

如果代码跑异常,打开浏览器,开始断点调试,发现InfoList这个变量第一次被赋值的时候是个数组:

[{name: 'test1', value: '11'}, {name: 'test2', value: '22'}]

过了一会竟然变成了一个对象:

{test1:'11', test2: '22'}

除了变量可以在运行时被赋值为任何类型以外,JavaScript 中也能实现继承,但它不像 Java、C++、C# 这些编程语言一样基于类来实现继承,而是基于原型进行继承。

这是因为 JavaScript 中有个特殊的存在:对象。每个对象还都拥有一个原型对象,并可以从中继承方法和属性。

提到对象和原型,有如下问题:

  1. JavaScript 的函数怎么也是个对象?
  2. proto和prototype到底是啥关系?
  3. JavaScript 中对象是怎么实现继承的?
  4. JavaScript 是怎么访问对象的方法和属性的?

原型对象和对象的关系

在 JavaScript 中,对象由一组或多组的属性和值组成:

{
  key1: value1,
  key2: value2,
  key3: value3,
}

在 JavaScript 中,对象的用途很是广泛,因为它的值既可以是原始类型(number、string、boolean、null、undefined、bigint和symbol),还可以是对象和函数。

不管是对象,还是函数和数组,它们都是Object的实例,也就是说在 JavaScript 中,除了原始类型以外,其余都是对象。

这也就解答了问题1:JavaScript 的函数怎么也是个对象?

在 JavaScript 中,函数也是一种特殊的对象,它同样拥有属性和值。所有的函数会有一个特别的属性prototype,该属性的值是一个对象,这个对象便是我们常说的“原型对象”。

我们可以在控制台打印一下这个属性:

function Person(name) {
  this.name = name;
}
console.log(Person.prototype);

打印结果显示为:

可以看到,该原型对象有两个属性:constructor和proto

到这里,我们仿佛看到疑惑 “2:proto和prototype到底是啥关系?”的答案要出现了。在 JavaScript 中,proto属性指向对象的原型对象,对于函数来说,它的原型对象便是prototype。函数的原型对象prototype有以下特点:

  • 默认情况下,所有函数的原型对象(prototype)都拥有constructor属性,该属性指向与之关联的构造函数,在这里构造函数便是Person函数;
  • Person函数的原型对象(prototype)同样拥有自己的原型对象,用proto属性表示。前面说过,函数是Object的实例,因此Person.prototype的原型对象为Object.prototype。

我们可以用这样一张图来描述prototype、proto和constructor三个属性的关系:

从这个图中,我们可以找到这样的关系:

  • 在 JavaScript 中,proto属性指向对象的原型对象;
  • 对于函数来说,每个函数都有一个prototype属性,该属性为该函数的原型对象;

使用 prototype 和 proto 实现继承

对象之所以使用广泛,是因为对象的属性值可以为任意类型。因此,属性的值同样可以为另外一个对象,这意味着 JavaScript 可以这么做:通过将对象 A 的proto属性赋值为对象 B,即:

A.__proto__ = B

此时使用A.proto便可以访问 B 的属性和方法。

这样,JavaScript 可以在两个对象之间创建一个关联,使得一个对象可以访问另一个对象的属性和方法,从而实现了继承;

使用prototype和proto实现继承

以Person为例,当我们使用new Person()创建对象时,JavaScript 就会创建构造函数Person的实例,比如这里我们创建了一个叫“zhangsan”的Person:

var zhangsan = new Person("zhangsan");

上述这段代码在运行时,JavaScript 引擎通过将Person的原型对象prototype赋值给实例对象zhangsan的proto属性,实现了zhangsan对Person的继承,即执行了以下代码:

//JavaScript 引擎执行了以下代码
var zhangsan = {};
zhangsan.__proto__ = Person.prototype;
Person.call(zhangsan, "zhangsan");

我们来打印一下zhangsan实例:

console.log(zhangsan)

结果如下图所示:

可以看到,zhangsan作为Person的实例对象,它的proto指向了Person的原型对象,即Person.prototype。

这时,我们再补充下上图中的关系:

从这幅图中,我们可以清晰地看到构造函数和constructor属性、原型对象(prototype)和proto、实例对象之间的关系,这是很多容易混淆。根据这张图,我们可以得到以下的关系:

  1. 每个函数的原型对象(Person.prototype)都拥有constructor属性,指向该原型对象的构造函数(Person);
  2. 使用构造函数(new Person())可以创建对象,创建的对象称为实例对象(lily);
  3. 实例对象通过将proto属性指向构造函数的原型对象(Person.prototype),实现了该原型对象的继承。

那么现在,关于proto和prototype的关系,我们可以得到这样的答案:

  • 每个对象都有proto属性来标识自己所继承的原型对象,但只有函数才有prototype属性;
  • 对于函数来说,每个函数都有一个prototype属性,该属性为该函数的原型对象;
  • 通过将实例对象的proto属性赋值为其构造函数的原型对象prototype,JavaScript 可以使用构造函数创建对象的方式,来实现继承。

所以一个对象可通过proto访问原型对象上的属性和方法,而该原型同样也可通过proto访问它的原型对象,这样我们就在实例和原型之间构造了一条原型链。红色线条所示:

通过原型链访问对象的方法和属性

当 JavaScript 试图访问一个对象的属性时,会基于原型链进行查找。查找的过程是这样的:

  1. 首先会优先在该对象上搜寻。如果找不到,还会依次层层向上搜索该对象的原型对象、该对象的原型对象的原型对象等(套娃告警);
  2. JavaScript 中的所有对象都来自Object,Object.prototype.proto === null。null没有原型,并作为这个原型链中的最后一个环节;
  3. JavaScript 会遍历访问对象的整个原型链,如果最终依然找不到,此时会认为该对象的属性值为undefined。

我们可以通过一个具体的例子,来表示基于原型链的对象属性的访问过程,在该例子中我们构建了一条对象的原型链,并进行属性值的访问:

var o = {a: 1, b: 2}; // 让我们假设我们有一个对象 o, 其有自己的属性 a  b:
o.__proto__ = {b: 3, c: 4}; // o 的原型 o.__proto__有属性 b  c:

当我们在获取属性值的时候,就会触发原型链的查找:

console.log(o.a); // o.a => 1
console.log(o.b); // o.b => 2
console.log(o.c); // o.c => o.__proto__.c => 4
console.log(o.d); // o.c => o.__proto__.d => o.__proto__.__proto__ == null => undefined

综上,整个原型链如下:

{a:1, b:2} —> {b:3, c:4} —> null, // 这就是原型链的末尾,即 null

可以看到,当我们对对象进行属性值的获取时,会触发该对象的原型链查找过程。

既然 JavaScript 中会通过遍历原型链来访问对象的属性,那么我们可以通过原型链的方式进行继承。

也就是说,可以通过原型链去访问原型对象上的属性和方法,我们不需要在创建对象的时候给该对象重新赋值/添加方法。比如,我们调用lily.toString()时,JavaScript 引擎会进行以下操作:

  1. 先检查lily对象是否具有可用的toString()方法;
  2. 如果没有,则“检查lily的原型对象(Person.prototype)是否具有可用的toString()方法;
  3. 如果也没有,则检查Person()构造函数的prototype属性所指向的对象的原型对象(即Object.prototype)是否具有可用的toString()方法,于是该方法被调用。

由于通过原型链进行属性的查找,需要层层遍历各个原型对象,此时可能会带来性能问题:

  1. 当试图访问不存在的属性时,会遍历整个原型链;
  2. 在原型链上查找属性比较耗时,对性能有副作用,这在性能要求苛刻的情况下很重要。

因此,我们在设计对象的时候,需要注意代码中原型链的长度。当原型链过长时,可以选择进行分解,来避免可能带来的性能问题。

其他方式实现继承

除了通过原型链的方式实现 JavaScript 继承,JavaScript 中实现继承的方式还包括经典继承(盗用构造函数)、组合继承、原型式继承、寄生式继承,等等。

  • 原型链继承方式中引用类型的属性被所有实例共享,无法做到实例私有;
  • 经典继承方式可以实现实例属性私有,但要求类型只能通过构造函数来定义;
  • 组合继承融合原型链继承和构造函数的优点,它的实现如下:
function Parent(name) {
  // 私有属性,不共享
  this.name = name;
}
// 需要复用、共享的方法定义在父类原型上
Parent.prototype.speak = function() {
  console.log("hello");
};
function Child(name) {
  Parent.call(this, name);
}
// 继承方法
Child.prototype = new Parent();

组合继承模式通过将共享属性定义在父类原型上、将私有属性通过构造函数赋值的方式,实现了按需共享对象和方法,是 JavaScript 中最常用的继承模式。

虽然在继承的实现方式上有很多种,但实际上都离不开原型对象和原型链的内容,因此掌握proto和prototype、对象的继承等这些知识,是我们实现各种继承方式的前提条件。

总结

关于 JavaScript 的原型和继承,常常会在我们面试题中出现。随着 ES6/ES7 等新语法糖的出现,可能更倾向于使用class/extends等语法来编写代码,原型继承等概念逐渐变淡。

其次JavaScript 的设计在本质上依然没有变化,依然是基于原型来实现继承的。如果不了解这些内容,可能在我们遇到一些超出自己认知范围的内容时,很容易束手无策。

手把手教你使用CanvasAPI打造一款拼图游戏

一、canvas简介

  1. canvas是HTML5提供的一种新标签,双标签;
  2. HTML5 canvas标签元素用于图形的绘制,通过脚本 (通常是JavaScript)来完成;
  3. canvas标签只是图形容器,必须使用脚本来绘制图形;

Canvas是一个矩形区域的画布,可以用JavaScript在上面绘画;

二、案例目标

我们今天的目标是使用HTML5画布技术制作一款拼图小游戏,要求将图像划分为3*3的9块方块并打乱排序,用户可以移动方块拼成完整图片。

效果如下所示:

手把手教你使用CanvasAPI打造一款拼图游戏

三、程序流程

3.1 HTML静态页面布局

<div id="container">
            <!--页面标题-->
            <h3>HTML5画布综合项目之拼图游戏</h3>
            <!--水平线-->
            <hr />
            <!--游戏内容-->
            <!--游戏时间-->        
            <div id="timeBox">
                共计时间:<span id="time">00:00:00</span>
            </div>
            <!--游戏画布-->
            <canvas id="myCanvas" width="300" height="300" style="border:1px solid">
                对不起,您的浏览器不支持HTML5画布API。
            </canvas>
            <!--游戏按钮-->
            <div>
                <button onclick="restartGame()">
                    重新开始
                </button>
            </div>  
</div>

效果如下所示:

手把手教你使用CanvasAPI打造一款拼图游戏

我们可以看到页面的大致结构是已经显现出来了,就是骨架已经搭建好了,现在我们要使用css强化样式;

3.2 CSS打造页面样式

整体背景设置

body {
    background-color: silver;/*设置页面背景颜色为银色*/
}

游戏界面样式设置

#container {
    background-color: white;
    width: 600px;   
    margin: auto;
    padding: 20px;
    text-align: center; 
    box-shadow: 10px 10px 15px black;
}

游戏时间面板样式设置

#timeBox {
    margin: 10px 0;
    font-size: 18px;
}

游戏按钮样式设置

button {
    width: 200px;
    height: 50px;
    margin: 10px 0;
    border: 0;
    outline: none;
    font-size: 25px;
    font-weight: bold;
    color: white;  
    background-color: lightcoral;
}

鼠标悬浮时的按钮样式设置

button:hover {
    background-color: coral;
}

设置好界面整体样式之后我们得到完整的界面,如下所示:

手把手教你使用CanvasAPI打造一款拼图游戏

可以看到整体的静态界面已经搭建出来了

3.3 js构建交互效果

3.3.1 对象的获取以及图片的设置

目标对象的获取

var c = document.getElementById('myCanvas'); //获取画布对象
var ctx = c.getContext('2d'); //获取2D的context对象

声明拼图的图片素材来源

var img = new Image();
img.src = "image/pintu.jpg";
                
img.onload = function() { //当图片加载完毕时
    generateNum(); //打乱拼图的位置
    drawCanvas(); //在画布上绘制拼图
}

3.3.2 初始化拼图

  • 需要将素材图片分割成3行3列的9个小方块,并打乱顺序放置在画布上;
  • 为了在游戏过程中便于查找当前的区域该显示图片中的哪一个方块,首先为原图片上的9个小方块区域进行编号;

定义初始方块位置

var num = [[00, 01, 02], [10, 11, 12], [20, 21, 22]];

打乱拼图的位置

function generateNum() { //循环50次进行拼图打乱    
         for (var i = 0; i < 50; i++) {
      //随机抽取其中一个数据
            var i1 = Math.round(Math.random() * 2);
            var j1 = Math.round(Math.random() * 2);
      //再随机抽取其中一个数据
            var i2 = Math.round(Math.random() * 2);
            var j2 = Math.round(Math.random() * 2);
      //对调它们的位置
            var temp = num[i1][j1];
            num[i1][j1] = num[i2][j2];
            num[i2][j2] = temp;
   }
}

绘制拼图

自定义名称的drawCanvas()方法用于在画布上绘制乱序后的图片;

function drawCanvas() {
    //清空画布
    ctx.clearRect(0, 0, 300, 300);
    //使用双重for循环绘制3x3的拼图
    for (var i = 0; i < 3; i++) {
        for (var j = 0; j < 3; j++) {
            if (num[i][j] != 22) {
                //获取数值的十位数,即第几行
                var row = parseInt(num[i][j] / 10);
                //获取数组的个位数,即第几列
                var col = num[i][j] % 10;
                //在画布的相关位置上绘图
                ctx.drawImage(img, col * w, row * w, w, w, j * w, i * w, w, w); // w:300 / 3 = 100(小图宽度)
            }
        }
    }
}

如下所示:

手把手教你使用CanvasAPI打造一款拼图游戏

3.3.3 事件绑定

监听鼠标监听事件

c.onmousedown = function(e) {
    var bound = c.getBoundingClientRect(); //获取画布边界
    
    var x = e.pageX - bound.left; //获取鼠标在画布上的坐标位置(x,y)
    var y = e.pageY - bound.top;


    var row = parseInt(y / w); //将x和y换算成几行几列
    var col = parseInt(x / w);


    
    if (num[row][col] != 22) { //如果当前点击的不是空白区域
        detectBox(row, col); //移动点击的方块
        drawCanvas(); //重新绘制画布
        var isWin = checkWin(); //检查游戏是否成功
        
        if (isWin) { //如果游戏成功
            clearInterval(timer); //清除计时器
            ctx.drawImage(img, 0, 0); //绘制完整图片
            ctx.font = "bold 68px serif"; //设置字体为加粗、68号字,serif
            ctx.fillStyle = "red"; //设置填充色为红色
            ctx.fillText("游戏成功!", 20, 150); //显示提示语句
        }
    }
}

点击方块移动

function detectBox(i, j) {
    //如果点击的方块不在最上面一行
    if (i > 0) {
        //检测空白区域是否在当前方块的正上方
        if (num[i-1][j] == 22) {
            //交换空白区域与当前方块的位置
            num[i-1][j] = num[i][j];
            num[i][j] = 22;
            return;
        }
    }
    //如果点击的方块不在最下面一行
    if (i < 2) {
        //检测空白区域是否在当前方块的正下方
        if (num[i+1][j] == 22) {
            //交换空白区域与当前方块的位置
            num[i+1][j] = num[i][j];
            num[i][j] = 22;
            return;
        }
    }
    //如果点击的方块不在最左边一列
    if (j > 0) {
        //检测空白区域是否在当前方块的左边
        if (num[i][j - 1] == 22) {
            //交换空白区域与当前方块的位置
            num[i][j - 1] = num[i][j];
            num[i][j] = 22;
            return;
        }
    }
    //如果点击的方块不在最右边一列
    if (j < 2) {
        //检测空白区域是否在当前方块的右边
        if (num[i][j + 1] == 22) {
            //交换空白区域与当前方块的位置
            num[i][j + 1] = num[i][j];
            num[i][j] = 22;
            return;
        }
    }
}

3.3.4 游戏计时

  • 自定义函数getCurrentTime()用于进行游戏计时;
  • function getCurrentTime() {
    s = parseInt(s); //将时分秒转换为整数以便进行自增或赋值
    m = parseInt(m);
    h = parseInt(h);
    s++; //每秒变量s先自增1
    if (s == 60) {
    s = 0; //如果秒已经达到60,则归0
    m++; //分钟自增1
    } if (m == 60) {
    m = 0; //如果分钟也达到60,则归0
    h++; //小时自增1
    //修改时分秒的显示效果,使其保持两位数
    if (s < 10)
    s = “0” + s;
    if (m < 10)
    m = “0” + m;
    if (h < 10)
    h = “0” + h;
    time.innerHTML = h + “:” + m + “:” + s; //将当前计时的时间显示在页面上
    }
  • 在JavaScript中使用setInterval()方法每隔1秒钟调用getCurrentTime()方法一次,以实现更新效果;var timer = setInterval(“getCurrentTime()”, 1000);

3.3.5 游戏成功与重新开始

游戏成功判定与显示效果的实现

  • 自定义函数checkWin()用于进行游戏成功判断;
function restartGame() {
    clearInterval(timer);  //清除计时器
    s = 0; //时间清零
    m = 0;
    h = 0;
    getCurrentTime();  //重新显示时间
    timer = setInterval("getCurrentTime()", 1000);
 
    generateNum(); //重新打乱拼图顺序
    drawCanvas(); //绘制拼图
    
}
  • 如果成功则使用clearInterval()方法清除计时器。然后在画布上绘制完整图片,并使用fillText()方法绘制出“游戏成功”的文字图样;if (isWin) { //如果游戏成功
    clearInterval(timer); //清除计时器
    ctx.drawImage(img, 0, 0); //绘制完整图片
    ctx.font = “bold 68px serif”; //设置字体为加粗、68号字,serif
    ctx.fillStyle = “red”; //设置填充色为红色
    ctx.fillText(“游戏成功!”, 20, 150); //显示提示语句
    }

3.4 最终效果演示

手把手教你使用CanvasAPI打造一款拼图游戏

静态效果如上所示,至于游戏成功这里伙计们可以自行操作;

四、总结

本次案例我们使用HTML5的新特性canvas画布标签打造了简单的9宫格拼图游戏,总体来说没有特别的复杂,主要是图片的分割方块移动事件的绑定,以及重新游戏的初始化操作,明确了游戏逻辑之后其实代码的编写其实不难。感兴趣的小伙伴可以去尝试一下。

一文带你解读​JavaScript中的变量、作用域和内存问题

一、基本类型和引用类型的值

  • 基本类型值:简单的数据段;
  • 引用类型值:多个值构成的对象;

回顾:

基本数据类型:undefined;null;number;boolean;string;按照值访问的,可以操作保存在变量中的实际的值;

引用数据类型:例如Array;不能直接访问值,它是保存在内存中的对象;

JavaScript不允许直接访问内存中的位置;即不能直接操作对象的内存空间;

我们在操作对象时,其实是操作对象的引用,而不是对象;

注意:如果我们复制保存着某个对象的变量时,那么两个变量就会指向同一个对象,当我们为对象添加属性时,操作的就是实际的对象;

1.1 动态的属性

  • 引用类型
var person = new Object() // 创建一个对象
person.name = '张三' // 设置对象属性
console.log(person.name) // 输出对象属性

这个属性会一直伴随着对象,除非对象销毁,否则该属性会一直存在;

  • 基本类型
var name = 'Nick'
name.age = 20
console.log(name.age) // undefined

只有引用值可以动态添加后面可以使用的属性;

1.2 复制变量值

  • 基本类型
var s = 'hello'
var s1 = s
console.log(s1) // 'hello'
console.log(s1 == s) // true

解释:

再新创建一个变量s1,它的值和s一样,都是字符型’hello’,所以s1 == s;两者完全独立,互不干扰;

  • 引用类型
var obj1 = new Object()
var obj2 = obj1
obj1.name = 'nick'
console.log(obj2.name)
console.log(obj2 == obj1)

图示:

一文带你解读​JavaScript中的变量、作用域和内存问题

我们的变量名obj1储存的是一个对象的引用,它指向堆里面的一个对象(object),通过复制,我们只是复制了一个变量obj2,它的指向和obj1一样都是指向object,所以设置完obj1.name = ‘nick’,之后修改的是指向的对象的属性,由于obj2也是指向这个对象,所以obj2.name = ‘nick’;

1.3 传递参数

函数的传参类似于我们变量的复制,我们来查看一下;

1.3.1 基本类型的传参

function addnum(num){
    num += 10
    return num
}
var count = 20
res = addnum(count)
console.log(count) // 20
console.log(res) // 30

解释:参数作为函数的局部变量,其实并不会对全局变量造成影响,所以count还是20;

1.3.2 引用类型的传参

function test(obj){
     obj.age = 20
}
var obj1 = new Object()
test(obj1)
console.log(obj1.age) // 20

解释:此处obj和obj1引用的是同一个对象;那么问题来了,针对于引用类型,参数的传递是按照值还是按照引用呢?看下面的例子:

function test(obj){
     obj.age = 20
     obj = new Object()
     obj.age = 21
}
var obj1 = new Object()
test(obj1)
console.log(obj1.age) // 20

这里如果是按照引用传递,obj1的指向应该变成函数内部创建的对象,并且其age值为21,但是实际输出为20,说明即使在函数内部修改了参数的值,其原始引用仍未改变;

函数内部创建的obj会随着函数调用结束而被销毁;

二、作用域

2.1 执行环境和作用域

执行环境: 定义了变量或函数有权访问的其它数据,决定了它们的行为。

全局执行环境是最外层的执行环境。根据 ECMAScript实现的宿主环境,表示全局执行环境的对象可能不一样。在浏览器中,全局执行环境就是我们常说的 window 对象。

执行环境中的代码在执行的时候,会创建变量对象的一个作用域链(scope chain)。这个作用域链决定了各级上下文中的代码在访问变量和函数时的顺序。

代码正在执行的执行环境的变量对象始终位于作用域链的最前端。如果上下文是函数,则其活动对象(activation object)用作变量对象。活动对象最初只有一个定义变量:arguments 。(全局执行环境中没有这个变量。)

作用域链中的下一个变量对象来自包含执行环境,再下一个对象来自再下一个包含执行环境。以此类推直至全局执行环境;全局执行环境的变量对象始终是作用域链的最后一个变量对象。

代码执行时的标识符解析是通过沿作用域链逐级搜索标识符名称完成的。搜索过程始终从作用域链的最前端开始,然后逐级往后,直到找到标识符。(如果没有找到标识符,那么通常会报错。)

var color = "blue";
function changeColor() {
    let anotherColor = "red";
    function swapColors() {
        let tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
        // 这里可以访问 color、anotherColor 和 tempColor
}
    swapColors();// 这里可以访问 color 和 anotherColor,但访问不到 tempColor
}


changeColor();// 这里只能访问 color

以上代码涉及 3 个执行环境:全局执行环境、 changeColor() 的局部执行环境和 swapColors() 的局部执行环境。全局执行环境中有一个变量 color 和一个函数 changeColor() 。changeColor() 的局部执行环境中有一个变量 anotherColor 和一个函数 swapColors() ,但在这里可以访问全局上下文中的变量 color 。其它函数同理;

2.2 延长作用域链

虽然执行环境主要有全局环境和局部环境两种,但有其他方式来延长作用域链。某些语句会导致在作用域链前端临时添加一个变量对象,这个对象在代码执行后会被删除。通常在两种情况下会出现这个现象,即代码执行到下面任意一种情况时:

  • try / catch 语句的 catch 块;
  • with 语句;

这两种情况下,都会在作用域链前端添加一个变量对象。对 with 语句来说,会向作用域链前端添加指定的对象;对 catch 语句而言,则会创建一个新的变量对象,这个变量对象会包含要抛出的错误对象的声明。如下所示:

function buildUrl() {
    let qs = "?debug=true";
    with(location){
        let url = href + qs;
}
    return url;
}

这里, with 语句接收 location 对象,因此 location 会被添加到作用域链前端。buildUrl() 函数中定义了一个变量 qs 。当 with 语句中的代码引用变量 href 时,实际上引用的是location.href ,也就是自己变量对象的属性。在引用 qs 时,引用的则是定义在buildUrl() 中的那个变量,它位于函数环境的变量对象中;至于with语句内部,则定义了一个url的变量,因而url变成函数执行环境的一部分,可以作为函数的值被返回;

2.3 没有块级作用域

if(true){
    var color = 'red'
}
console.log(color) // red

这里我们很疑惑,这个color在{}中,不应该是局部变量吗?为什么在全局中也能够输出;

解释:在这里if语句声明的变量将会添加到当前的执行环境(即全局环境),使用for语句也是一样;

for(var i = 0;i < 5;i++){
   console.log('i')
}
console.log(i) // 5

声明变量

使用var声明的变量会被自动添加到最接近的环境中,在函数内部声明,最接近的环境就是函数的局部环境;在with语句中,最接近的环境就是函数环境;如果没有使用var声明变量,那么就会自动添加到全局环境中;

function test(a,b){
    var sum = a + b
    return sum
}
console.log(test(10,20)) // 30
console.log(sum) // ReferenceError: sum is not defined

这里原因就不做过多解释了,但是如果我们在该函数内部省略var,直接声明sum,那么在函数外部也是可以输出sum的,因为此时他就是一个全局变量;

在JavaScript中,不声明而直接初始化变量是一种错误做法;

三、垃圾回收

3.1 垃圾回收机制

JavaScript 是使用垃圾回收的语言,也就是说执行环境负责在代码执行时管理内存。JavaScript 通过自动内存管理实现内存分配和闲置资源回收。

  • 基本过程:确定某个变量不会再使用,然后释放它占用的内存。

这个过程是周期性的,即垃圾回收程序每隔一定时间就会自动运行。垃圾回收过程是一个近似且不完美的方案,因为某块内存是否还有用,属于“不可判定的”问题,意味着靠算法是解决不了的。

3.2 性能问题

垃圾回收程序会周期性运行,如果内存中分配了很多变量,则可能造成性能损失,因此垃圾回收的时间调度很重要。尤其是在内存有限的移动设备上,垃圾回收有可能会明显拖慢渲染的速度和帧速率。

现代垃圾回收程序会基于对 JavaScript 运行时环境的探测来决定何时运行。探测机制因引擎而异,但基本上都是根据已分配对象的大小和数量来判断的。

由于调度垃圾回收程序方面的问题会导致性能下降,它的策略是根据分配数,比如分配了 256 个变量、4096 个对象/数组字面量和数组槽位(slot),或者 64KB 字符串。只要满足其中某个条件,垃圾回收程序就会运行。

这样实现的问题在于,分配那么多变量的脚本,很可能在其整个生命周期内始终需要那么多变量,结果就会导致垃圾回收程序过于频繁地运行。

由于对性能的严重影响,IE7最终更新了垃圾回收程序。IE7 发布后,JavaScript 引擎的垃圾回收程序被调优为动态改变分配变量、字面量或数组槽位等会触发垃圾回收的阈值。IE7 的起始阈值都与 IE6 的相同。如果垃圾回收程序回收的内存不到已分配的 15%,这些变量、字面量或数组槽位的阈值就会翻倍。如果有一次回收的内存达到已分配的 85%,则阈值重置为默认值。这么一个简单的修改,极大地提升了重度依赖 JavaScript 的网页在浏览器中的性能。

3.3 管理内存

为什么需要管理内存?

在使用垃圾回收的编程环境中,JavaScript 运行在一个内存管理与垃圾回收都很特殊的环境。分配给浏览器的内存通常比分配给桌面软件的要少很多,分配给移动浏览器的就更少了。这更多出于安全考虑而不是别的,就是为了避免运行大量 JavaScript 的网页耗尽系统内存而导致操作系统崩溃。这个内存限制不仅影响变量分配,也影响调用栈以及能够同时在一个线程中执行的语句数量。

接触引用

将内存占用量保持在一个较小的值可以让页面性能更好。优化内存占用的最佳手段就是保证在执行代码时只保存必要的数据。如果数据不再必要,那么把它设置为 null ,从而释放其引用。

局部变量在超出作用域后会被自动解除引用,如下所示:

function createPerson(name){
    let localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}
let globalPerson = createPerson("Nicholas"); // 解除 globalPerson 对值的引用
globalPerson = null;

在上面的代码中,变量 globalPerson 保存着 createPerson() 函数调用返回的值。在 createPerson()内部, localPerson 创建了一个对象并给它添加了一个 name 属性。然后, localPerson 作为函数值被返回,并被赋值给 globalPerson 。localPerson 在 createPerson() 执行完成超出执行环境后会自动被解除引用,不需要显式处理。但 globalPerson 是一个全局变量,应该在不再需要时手动解除其引用,最后一行就是这么做的。不过要注意,解除对一个值的引用并不会自动导致相关内存被回收。解除引用的关键在于确保相关的值已经不在执行环境里了,因此它在下次垃圾回收时会被回收。