前端系统课程 - 27. DOM API

DOM 树

  • 文档内所有的元素(Element),在 JavaScript 中都有相应的对象,而这些元素或对象,都可以用树形结构表示出来,这个像模型似的属性结构,就是 DOM 树。

  • DOM 中的每个元素,都是节点(Node),因为它们都是 Node 对象的实例。

  • <html> 元素是 document 对象的唯一实例。

  • Node 对象根据不同的构造函数,构造出不同的节点;主要的有 documentElementText 等等。

DOM API

  • elem.childNodes 可以获取元素的所有类型的子节点,其中当然包括文本节点、注释节点等,而不是单单获取元素节点。

  • elem.children 可以获取元素的子元素,也就是子标签,只有在 IE 中可能错误地一并获取到注释节点,但是这点不足以掩盖这个方法很好用。

  • 在 DOM 中,document.documentElement 这个属性代表 <html>,虽然 <html> 是根节点,但是在 DOM 里,它还是有爹的,他爹就是 document 对象。

  • <svg> 标签(元素)的 nodeName 属性是小写字母的“svg”,其他标签使用 nodeName 获取标签名,返回的都是大写字母。

  • textContentinnerText 主要的不同点在于:

    • textContent 会获取元素内的所有文本内容,包括 <script>style 元素内的,而 innerText 不会;

    • textContent 会获取隐藏元素的文本,而 innerText 不会;

    • innerText 会触发重排,而 textContent 则不会;

    • innerText 会替换元素里的内容,而 textContent 则不会;

    • 综上所述,修改文本内容 textContent 适用面更广。

  • cloneNode() 方法可以复制一个节点,它可以接收一个参数;如果传入 ture,便是深拷贝,会将目标节点的所有子节点都复制下来;如果不传,便是浅拷贝,只会复制目标节点本身。

  • isEqualNode() 方法可以判断两个节点是否相同,也就是判断它们是否为同一种节点。

  • isSameNode() 方法可以判断两个节点是否相等,也就是判断它们是否为同一个节点。

  • 使用 normalize() 方法可以使元素常规化,用来去除一些不必要的混乱。

  • 使用 document.visibilityState 可以获取当前页面的焦点状态(阅读状态)。

  • 搞清楚单词意思,一般就能理解这些接口的用法;难懂的就去查文档。

课后拾遗

  • elem.childNodes 返回的是动态集合;所谓动态集合就是一个活的集合,DOM 树删除或新增一个相关节点,都会立刻反映在 NodeList 接口之中,所以它的长度会随着集合内元素的改变而变化。

  • querySelectorAll() 方法返回的是一个静态集合;DOM 树内部的变化,并不会实时反映在该方法的返回结果之中,所以该方法返回的集合长度,是不会随着集合内元素的改变而变化。

  • HTMLCollection (元素集合)对象与 NodeList (节点集合)对象的区别:

    • HTMLCollection 实例对象的成员只能是 Element 类型的节点,NodeList 实例对象的成员可以包含其他节点。

    • HTMLCollection 实例对象都是动态集合,节点的变化会实时反映在集合中;NodeList 实例对象可能是静态集合,也可能是动态集合。

    • HTMLCollection 实例对象可以用 id 属性或 name 属性引用节点元素,NodeList 只能使用数字索引引用。

前端系统课程 - 26. JavaScript 函数

声明函数

  • 函数声明的方式有:

    • 使用 function 操作符声明函数,这种方式又分为具名函数、匿名函数和函数表达式。

    • 使用全局方法 Function()

    • 使用 λ(拉姆达)表达式,又称为“箭头函数”(ES6 新增),这种函数都是匿名函数;当函数内只有一个 return 语句时,花括号和 return 关键字都可以(要一并)省略;函数参数只有一个时,包裹参数的括号可以省略;例如:

      (x, y) => {return x + y} 可简写为 (x, y) => x + y

      (x) => {return x * 2} 可简写为 x => x * 2

  • 匿名函数无法直接使用,需要赋值为一个变量,或者将其转换为表达式。

  • 如果将一个具名函数赋值给一个变量,那么它的函数名会变更为变量名。

  • 函数有一个 name 属性,如果是匿名函数,这个值是空字符串;如果是具名函数,这个值是函数声明时的名称,即使将这个函数赋值给一个变量,也不会改变,但是如果此时使用这个名字访问函数,是无法访问的;而匿名函数赋值给一个变量后,这个值会变成变量名,实际上此时,匿名函数已成为具名函数,name 值也就遵循具名函数的特性。

  • 关于函数的 name 属性,还有一个特殊情况:使用 new Function() 方式声明的函数,它的 name 属性值是字符串 'anonymous',意思是“匿名的”,也无法通过赋值操作改变,但又无法使用这个名字调用这个函数……

函数的本质

  • 函数是一段可以反复调用的代码块;函数还能接收输入的参数,不同的参数会返回不同的值。

  • 要了解函数的本质,要先了解 eval() 这个全局方法,它可以将传入的字符串,计算后当作脚本代码来执行。

  • 函数的调用,实际上是在调用函数的 call() 方法,其内部有一个类似 eval() 的方法将函数体解析为脚本代码后运行;直接使用函数名后加括号的调用方法,实际上是 JavaScript 的一种语法糖,真正的“硬核(hard cored)” 方法是函数调用自己的 call() 方法。

  • 从前面的内容可以看出,函数是一个可以执行代码的对象。

  • 函数原型包含的三个重要方法:call()apply()bind()

谈谈 this 和 arguments

  • 函数的 call() 方法中的第一个参数,可以用 this 得到,其他的参数,可以用 arguments 得到,前面说过,arguments 是一个伪数组。

  • 在普通模式(相对于严格模式)下,this 的值如果是 undefined,JavaScript 会将其自动引用为 window 对象(全局对象),这是一个“潜规则”。

  • JavaScript 中的 newthis 都是要模拟 Java 语言,但是画虎不成反类其犬……

调用栈 Call Stack

  • 函数调用分为:普通调用、嵌套调用和递归调用(自己嵌套调用自己)。

  • 函数被调用时,都会在调用栈里记录一个位置;嵌套调用也一样,当调用并完成相应的运算后,会跳回之前记录的位置,并将记录抹去,再继续往下运行,直至函数结束调用。

  • 当函数未运行完成时,它会暂时保存在调用栈里,这个叫做“压栈”;递归调用和嵌套调用都会压栈。

  • 当压栈超过系统提供的栈的最大值时,运行就会报错,称为“栈溢出(Stack Overflow)”。

作用域

  • 当遇到一些关于作用域的问题,一定不要忘记变量声明提前这回事。

  • 作用域按照语法树,遵循就近原则。

  • 作用域与变量的关系,主要理解的是变量在哪个作用域,变量是哪一个变量,而不是变量是哪一个变量的值,因为变量的值是可变的。

  • 理解了作用域和变量的关系,以及变量与值的关系,就能理解为什么在循环为一个菜单列表添加事件后,触发事件时打印出的下标,都是列表的长度而不是每个列表项对应的下标;因为当你触发事件的时候,循环已经完成并停止,此时的循环下标,已经是列表的长度了,虽然下标还是那个下标,但是值已经发生了改变。

闭包

  • 如果一个函数使用了它范围外的变量,那么(这个函数加这个变量)就叫做闭包。

课后拾遗

  • 在严格模式下('use strict'),this 值如果是基本类型,那么打印时便是这个值,而普通模式下,打印时是这个值包装的对象。

  • 语句都会返回 undefined

  • 多用硬核方式调用函数,fn.call(undefined, arg1, arg2...),就会理解每个函数都有自己的 this

前端系统课程 - 25. JavaScript 数组

标准库

  • 在 JavaScript 内部预先内置的属性和方法,也就是内置 API,称为标准库。

  • 在 3 个基本数据类型对应的全局方法前加 new 操作符和不加的区别是:加了会把参数里的数据转换成对象,不加的话只进行相应的转换;至于函数和数组,加不加无所谓。

数组

  • 数组和对象的区别,根本在于它们的公共属性不同,也就是 __proto__ 的原型链不同。

  • 用对象可以模拟数组存储数据的方式,但由于原型链中没有 Array.prototype 这一链,所以它没有数组的特性,那么这种貌似数组的“数组”就是“伪数组”;典型的伪数组是函数参数列表 arguments 对象以及 DOM API 返回的元素集合。

  • 使用 forEach() 方法可以遍历调用它的数组,它的参数必需接收一个函数(另一个参数是可选的),这个函数接收三个参数,第一个参数是必需的,代表数组的每一项数据,第二个是数据对应的下标,第三个是数据对应的数组。

  • forEach() 循环的数组,是调用它的数组,是用 this 操作符实现的;然后在内部循环这个数组,分别将其每项数据和下标返回以供使用。

  • 数组的排序方法 sort() 默认将数组内数据按字符编码顺序排序,这个排序会改变原数组;如果要按其他标准排序,可以传入一个比较函数;该函数要比较两个值,然后返回一个用于说明这两个值的相对顺序的数字。比较函数应该具有两个参数 a 和 b,其返回值如下:

    • 若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。

    • 若 a 等于 b,则返回 0。

    • 若 a 大于 b,则返回一个大于 0 的值。

  • join() 方法可以使用指定字符串连接数组各项后,将数组转换为一个字符串后返回这个字符串,原数组不受影响。

  • concat() 方法可以将多个数组拼合成一个新数组后返回这个新数组,原数组不受影响;还可以利用它返回一个新数组的特性,来完全复制一个数组。

  • map() 方法和 forEach() 方法遍历数组的功能完全一样,但区别在于:

    • map() 可以将原数组的每个元素在调用函数处理后的值,返回为一个新数组;也可以利用这些返回值组成任意想要的值来返回;map() 不会检测空数组。

    • forEach() 本身没有返回值,但是它会忽略空数组。

  • filter() 方法可以按照指定的条件,过滤掉数组中不符合这个条件的数组元素,并将符合条件的元素作为一个新数组返回;它接收一个函数作为参数,这个函数接收三个参数,可以参考 forEach() 方法内的函数参数。

  • reduce() 方法的第一个参数是一个函数,此函数称为“累加器”,数组中的每个值从左到右开始缩减,最终计算称为一个值并返回;除了累加器,reduce() 的另一个参数是一个传递给参数的初始值,也就是说,以这个初始值为基数进行计算;累加器的参数有四个,第一个是必需值,表示初始值或者每次计算后的返回值,它会参与到下一次计算;第二个也是必须值,表示数组中的当前元素;剩下的两个一个是当前元素的索引,另一个则是当前元素所属的数组,这两个非必须值。例子:

    1
    2
    3
    4
    5
    var arr = [2, 4, 6, 8];
    var total = arr.reduce(function (previousValue, currentValue) {
    return previousValue + currentValue;
    }, 10);
    console.log(total); // 30
  • 使用 reduce() 方法可以模拟 map()filter() 方法,示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // map方法
    var mapArr = arr.map(function (value) {
    return value * 2;
    });
    console.log(mapArr.join()); // 2, 4, 6, 8, 10, 12, 14, 16, 18, 20

    // 模拟map方法
    var mapArrRe = arr.reduce(function (previousValue, currentValue) {
    previousValue.push(currentValue * 2);
    return previousValue;
    }, []);
    console.log(mapArrRe.join()); // 2, 4, 6, 8, 10, 12, 14, 16, 18, 20

    // filter方法
    var filterArr = arr.filter(function (value) {
    return value % 2 === 0;
    });
    console.log(filterArr.join()); // 2, 4, 6, 8, 10

    // 模拟filter方法
    var filterArrRe = arr.reduce(function (previousValue, currentValue) {
    if (currentValue % 2 === 0) {
    previousValue.push(currentValue);
    }
    return previousValue;
    }, []);
    console.log(filterArrRe.join()); // 2, 4, 6, 8, 10

前端系统课程 - 24. 给简历添加 JavaScript (2)

动画

  • 用 JavaScript 实现动画的关键是 window.setInterval() 这个全局方法,这个方法称为循环定时器,与前面学到的延迟定时器 setTimeout() 使用方法类似;

  • 页面滚动动画思路:

    • 使用 window.pageXOffset 属性获取当前页面滚动位置 currentTop,以及通过元素的 offsetTop 属性值来设定滚动目标位置 targetTop;

    • 设定到目标位置的滚动总次数 n,当前滚动次数 x;

    • 设定每次滚动的时间 t 为预设动画完成时间 / 滚动总次数,例如:

      t = 200 / n

    • 设定每次滚动的距离 step 为 (目标滚动位置 - 当前滚动位置) / 滚动总次数,即:

      step = (targetTop - currentTop) / n

    • 最后设置循环定时器,通过 window.scrollTo() 方法,在指定时间使页面滚动到指定位置,每次滚动之前,当前滚动次数加 1;在当前滚动次数达到设定的总次数后,说明已到达目标位置,此时便使用 clearInterval() 方法清除定时器,动画结束。下面是示例代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      var currentTop = window.pageYOffset, // 当前滚动位置
      targetTop = getOffsetTop(id) < 200 ? 80 : getOffsetTop(id), // 目标滚动位置
      n = 25, // 滚动总次数
      x = 0, // 当前滚动次数
      t = 200 / n, // 每次滚动时间
      step = (targetTop - currentTop) / n, // 每次滚动距离
      timer = setInterval(function () {
      if (x === n) { // 当前滚动次数达到总次数
      clearInterval(timer); // 清除定时器
      return; // 跳出函数
      }
      x++; // 当前次数自增
      window.scrollTo(0, currentTop + step * x) // 滚动到当前次数该到的位置
      }, t);

缓动

顾名思义,就是缓缓移动,让动画不那么生硬,就像 CSS 动画那样,可以设置不同的速度曲线,看起来有不同的线性效果。

  • 如果自己计算缓动函数(缓动函数速查表)难度比较高,所以借助现有的成熟库更方便,例如 tween.js,示例代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function animate(time) { // 设置动画循环时间
    requestAnimationFrame(animate);
    TWEEN.update(time);
    }
    requestAnimationFrame(animate);

    var coords = { x: 0, y: 0 }; // 起始坐标数据
    var tween = new TWEEN.Tween(coords) // 创建新实例并将坐标数据传入
    .to({ x: 300, y: 200 }, 1000) // 在1秒钟内移动到指定坐标
    .easing(TWEEN.Easing.Quadratic.Out) // 设置指定的缓动方式
    .onUpdate(function() { // 此函数会在Tween每次将坐标数据更新后调用
    // 在函数内部就可以使用坐标数据来完成动画
    })
    .start(); // 立即启动实例
  • 要学会在不懂的情况下搞定遇到的问题。

菜单高亮

通过在页面滚动时,计算不同区块相对顶部高度与当前页面滚动高度的差值,找到最小差值相对应的区块,让锚链接这个区块的菜单改变样式即可;由于差值可能是负数,比较时要先将差值全部取绝对值,可以使用内置数学对象的 Math.abs() 方法。

小结

还有一些其他动画效果,都很简单,就不一一赘述了;使用 JavaScript 操作 DOM 节点其实很简单,就是触发事件,然后找到相应节点,然后对其添加或删除类名,或者改变节点的其他属性,最终完成一个效果。

前端系统课程 - 23. 给简历添加 JavaScript (1)

置顶导航栏

英文 sticky navbar,通过监听页面滚动事件将导航栏设置为固定定位并置顶;涉及知识:

  • onscroll 页面滚动事件句柄,onmousewhell 鼠标滚轮滚动事件句柄。

  • 获取窗口滚动距离 window.scrollX/Y,建议使用 window.pageXOffset/YOffset 来代替。

加载动画

英文 loading animation,两个圆形从小变大,从深变浅,其中一个延迟动画即可;涉及知识:

  • window.setTimeout(function, time),延迟定时器。

  • 动画连写参数(参数之间无逗号):animation(动画名 完成时间 速度曲线 延迟开始 次数 是否轮流)

  • 使用 Compass 导入 keyframe 函数时,参数内是动画名,括号后加花括号,里面写动画顺序和内容。

滑动菜单

英文 slide menu,通过监听一级菜单的鼠标移入移除事件来将二级菜单显示或隐藏;涉及知识:

  • 鼠标移入移出事件句柄有两组,分别是:onmouseover / onmouseoutonmouseenter / onmouseleave;区别是:前者的子元素也会触发事件,后者的子元素不会触发事件;要分不同的情况来监听不同的事件。

  • 如何找到二级菜单,要看具体的元素结构;课程开始使用了 nextSiblling 方法,来获取一级菜单内链接的下一个兄弟节点(获取上一个兄弟节点的方法是 previousSibling),再通过 nodeType 属性不同的返回值判断节点类型(例如元素节点返回值为 1),还要判断元素的 tagName 属性(返回的标签名为大写字母)是否为预期的,如果需要向下获取并判断多次,可以使用 while 语句来循环寻找元素。

  • 自己实现的方法更接近原型,使用了 onmouseoveronmouseout 事件,这样二级菜单也可以同时触发显示动作,无需额外处理;CSS 方面,使用了定位属性和过渡动画做位移,代码更简洁,并且有回退动画效果;但是带来的问题是无法使用 display: none 来隐藏菜单,最后使用了 visibility: hidden/visible 来做隐藏和显示,虽然元素并未从页面完全消失,但不会影响体验。

  • 解决问题的关键在于,马上开始着手解决问题。

滑动定位

类似锚点,但带有滑动效果,就是让滚动条在指定时间内滚动到指定位置,可以使用相应元素距离页面顶部高度来设置位置;涉及知识:

  • 在获取元素对象的属性时,一般推荐使用 setAttribute()getAttribute() 方法,直接使用点运算符,可能会返回经过浏览器处理的结果,例如 <a> 标签中 href 的属性值。

  • 使用 document.querySelector() 方法可以返回文档中匹配括号中指定 CSS 选择器的第一个元素,如果要想返回匹配的所有元素,可以使用 document.querySelectorAll() 方法。

  • 点击 <a> 标签时,要阻止它的默认事件,可以使用 href="javascript:" 的方法,但是如果 href 属性另有他用怎么办?那就使用时间对象的阻止默认事件方法 ev.preventDefault()

  • 使用 offsetTop 属性可以获取元素相对于它的 offsetParent 元素的顶部的距离(不包含边框),这是一个只读属性,返回值是数字类型的像素数;由于无法直接获取到元素相对页面最顶端的距离,所以只能用这个属性结合 offsetParent 属性,一直获取到 <body> 标签以下的所有元素的 offsetTop 之和;与之相配套的属性是 offsetLeft 属性,代表相对左上角的距离。

  • 使用 window.scrollTo() 方法可以使页面滚动到指定位置,有两个必需参数,第一个是 x 坐标,第二个是 y 坐标。

前端系统课程 - 22. JavaScript 里的对象

全局对象

  • 浏览器是 JavaScript 的宿主环境之一;浏览器特有的全局对象(global)是 window 对象。

  • 全局对象的属性(或方法)分为公共属性和私有属性;公共属性是指 ECMAScript 规定的属性,而私有属性是不同的宿主环境特有的属性,例如浏览器中的全局对象是 window 对象,它的私有属性常见的例如:document 对象、history对象、alert()prompt()confirm() 等等。

基本数据类型与对象的区别

  • 基本数据类型没有且不能具备属性和方法,而复杂数据类型(对象)可以具备属性和方法。

  • 使用 new 运算符可以创建一个指定对象的实例,例如 var num = new Number(1)

  • 当我们平时在对存有基本数据类型的变量使用 toSring() 等方法时,由于数据没有这些方法,所以编译器会隐性地临时将基本数据类型转换为对象,然后再调用其指定的方法,得出结果并返回,之后临时对象会被销毁。

  • 当给一个基本数据类型添加属性时,编译器不会报错,它会像前面一样将基本数据类型转换为对象,再给这个临时对象添加属性,之后这个临时对象被销毁,也就访问不到添加给基本数据类型的属性了。

公共属性

  • 每个对象都有 __proto__ 这个隐藏属性,它内部保存着对象的公共属性,其实就是从父对象继承来的属性;它引用的是父对象的 prototype 属性,所有对象不管继承了多少层,最终都会有一条“链”引到 Object 对象的 prototype,这就是原型链。

  • 从上一条可以知道,在 JavaScript 中,所有的对象,包括内置对象,都是 Object 对象的实例。

  • 继承:实例.__proto__ === 父函数.protorype函数.prototype.__proto__ === Object.prototype

  • __proto__ 是用来引用父函数的 prototype 属性的,类似于上线下线的关系,而 __proto__ 就是下线。

函数的原型链

Function 对象的 __proto__ 属性引用的是 Function 自身的 prototype,这是内置对象中的特殊情况;而 Object 对象的 __proto__ 属性引用的也是 Function 对象的 prototype,这是因为 Object 对象也是由函数构造的。

前端系统课程 - 21. JavaScript 里的类型

常见数据类型的转换

  • 除了 Null 和 Undefined 类型,其他数据类型都可以使用 toString() 方法转换为字符串类型,Object 类型虽然有这个方法,但是不一定得到预期的结果(可以使用 JSON.stringify() 方法);比 toString() 方法更简单的就是将要转换的数据拼接一个空字符串,这个方法更简略也更强大,它实际是使用了隐式类型转换,通过全局方法 String() 将数据转换类型。

  • 使用全局方法 Boolean() 可以将所有数据类型转换为布尔值;简便方法可以在要转换的数据前,使用两个取反操作符 !!

  • 转换数字类型的方法有 Number()parseInt()parseFloat() 方法;简便方法可以直接在要转换的数据前加正数符号 +;或者使用数据减 0 的操作,例如 '3.14' - 0,字符串 3.14 就会被转为数字 3.14,这种比较常用。

内存中数据存储方式

  • 编译器会将分配到的内存分为代码区和数据区。

  • 数据区分为 Stack(栈内存) 和 Heap(堆内存)。

  • 堆比栈大,栈比堆快。

  • JavaScript 代码在编译起始,会先将声明的变量提到最前面。

  • 基本数据类型都保存在栈内存,复杂数据类型都保存在堆内存。

  • 当把一个基本数据类型赋值给一个变量时,变量中存储的就是这个数据本身;当把这个变量赋值给另外一个变量时,相当于将它保存的数据本身,复制一个拷贝,再赋值给新变量,之后,两个变量各拥有一份相互独立的数据。

  • 当把一个复杂数据类型赋值给一个变量时,变量中存储的是这个数据在堆内存中的地址,这种关系称为“引用”;当把这个变量赋值给另外一个变量时,相当于把保存的引用地址,复制一个拷贝,再赋值给新变量,之后,两个变量引用的是同一个数据,相当于两个人各拥有一张代表同一个数据的“名片”。

循环引用

对象自身循环引用可以实现,但是不能在声明对象的同时将其自身保存在内,因为编译的顺序是先根据赋值操作符也就是 = 号的右边的数据大小来开辟内存空间,然后再将存好的数据的地址,存入变量;如果要完成自身的循环引用,需要在对象声明并赋值完成后,再将其自身引用通过添加属性的方式保存到自身当中;循环引用可能会造成内存泄露

试题解析

1
2
3
4
5
var a = {n: 1};
var b = a; //b = {n: 1};
a.x = a = {n: 2}; //{n: 1, x: {n: 2}},a = {n: 2};
console.log(a.x); //undefined
console.log(b.x); //[object, Object]
  • 上面的题中,第三行的关键点在于确定当时的 a 变量是哪一个;

  • 编译器是从上到下依次执行,但是各种操作符除了有优先级之分,还有从右往左和从左往右两种关联性,而属性访问操作符优先级比赋值操作符的优先级要高,所以先确定了 a.xa 引用的依然是 {n: 1} 这个数据,并在这个数据中添加了一个属性 x,此时 x 只是声明后未定义等待赋值的状态,就是说源数据成为了 {n: 1, x: undefined}

  • 然后赋值运算符从右往左执行,先将 a 变量中的数据,赋值为了 {n: 2} 的地址,再将这个地址,赋值给了 {n: 1, x: undefined} 这个对象中的 x 属性,此时数据成为了 {n: 1, x: {n: 2}}

  • 第四行访问 a.x,此时的 a 引用的数据已经是 {n: 2} ,里面并没有 x 这个属性,故打印出的结果为 undefined

  • 第五行访问 b.x,此时的 b 引用的数据依然是原来包含 n: 1 键值的那个数据,而此时这个数据已经有了 x 这个属性,并且值为另一个对象引用地址,这个地址对应的数据是 {n: 2},将其隐性地使用 toString() 方法转换为字符串后,打印出的结果便是 [object Object]

垃圾回收

  • 如果一个对象没有被引用,它就是垃圾数据,将被垃圾回收器清理掉。

  • 垃圾回收器会在页面运行时,按照浏览器设定好的机制循环作业。

深拷贝与浅拷贝

  • 拷贝即复制,复制后的数据与原数据,完全独立且互不影响,就称为“深拷贝”;基本数据类型之间的复制,都是深拷贝。

  • 其实基本数据类型之间的复制谈不上什么”深浅“,深拷贝与浅拷贝主要是对于对象数据之间的复制而言的。

  • 如果将一个对象赋值给一个变量,实际是将这个对象的引用地址保存在里变量当中,那么当复制这个变量到另一个变量时,只是把这个引用地址,存到了新的变量中;当通过新的变量改变对象的属性后,访问原先的变量也能发现对象的属性变化;这种程度的复制,就是“浅拷贝”。

课后拾遗

  • JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。

  • 数组的某个元素为空元素(即两个逗号之间没有任何值)时,便称为这个数组存在空位;空位不影响数组的长度;如果读取空位,会返回 undefined,但是把一个元素赋值为 undefined 后,这个元素不属于空位;如果使用 delete 操作符删除某个元素,那么这个元素对应的位置便成为了空位;使用 for...in 语句、数组的 forEach() 方法以及对象的 keys() 方法遍历数组时, 空位都会被跳过。

前端系统课程 - 20. JavaScript 里的数据

JavaScript 历史

  • 1995 年,网景(Netscape)公司雇员 Brendan Eich 开发出了网页脚本语言 JavaScript 的 1.0 版。

  • 1997 年,ECMA 组织发布 262 号标准文件(ECMA-262)的第一版 ,ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现。

  • 2015 年,ECMA 组织正式批准了 ECMAScript 6 语言标准,定名为《ECMAScript 2015 标准》。

  • 了解更多历史,可以参考红宝书或阮一峰的书……

JavaScript 的数据类型

JavaScript 中有七种数据类型,分别是:

  • Number(数字)

  • String(字符串)

  • Boolean(布尔值)

  • Undefined(未定义)

  • Null(空)

  • Symbol(符号,ES6 版本新增)

  • Object(对象)

数据类型的扩展知识

  • Number 类型表示数字,数字一般以二进制、八进制、十进制及十六进制表示;由于进制转换的原因,数字如果有前导 0,转换后可能出现意外的 Bug(例如八进制转换),所以实际开发一般都使用十进制;如果不是参与运算的数字(例如手机号码),那么一般使用字符串存储。

  • String 类型表示字符串,空字符串只是一对单引号或双引号,其中只有一个空格也不是空字符串。

  • 包裹字符串的引号,一般推荐使用单引号,这样和 HTML 代码配合比较方便;另外,表示字符串的引号,是一种形式,并不是内容,可以理解为类似数组的方括号。

  • 区分正斜杠和反斜杠,可以参照汉字“八”,撇就是正斜杠,捺就是反斜杠;转义符是反斜杠,单行注释是双正斜杠。

  • 多行字符串可以在末尾换行处加一个转义符,但是这个转义符后面除了换行符不能有任何字符,一般很少使用这种方法,更推荐多行拼接的方法。

  • 将数据类型转换为 Boolean,也就是布尔值类型后,值为 false(假,称为“falsy”值)的有:falsenullundefined0NaN、空字符串 ''"";值为 true(真,称为“truthy”值)的有:任何非空字符串、任何对象(包括空对象)、任何非 0 的数值(包括无穷大和无穷小)。

  • 按照惯例,声明一个变量要存储一个对象,但一开始不准备具体赋值时,便将其赋值为 null,表示这个空变量将要存储一个对象而不是基本数据类型;而如果是要存储基本类型,只声明不赋值即可,值本来就是 undefined

  • 对象中的属性名可以是任意字符串,包括空字符串;如果属性名是数字,在访问时会自动转换为字符串,但是如果不是数字也不是字符串,则需要符合标识符的命名规则;然而如果属性名是字符串或数字,那么访问时就需要使用方括号运算符而不能使用点运算符的方式,除非字符串属性名符合标识符的命名规则。

  • 删除一个对象属性可以使用 delete 操作符,注意使用此操作符删除属性后,属性的键和值都会被删除。

  • 使用 typeof 操作符对函数进行检测,返回值为反直觉的 'function',而不是 'object';但是检测值为 null 的数据,则会返回 'object'……

课后拾遗

  • 访问对象的属性时,如果在方括号内的属性名是一个数字间运算,那么会先运算得出的结果数字,然后再将数字转换为字符串;

  • 要知道一个对象有哪些键名,可以使用对象的 keys() 方法。

前端系统课程 - 19. Canvas 画板 (2)

视口设置

原先的电脑页面在手机上显示,由于屏幕缩小,手机只能将页面缩小后再显示,而想要让页面适配到手机屏幕,就要给页面设置视口适配。设置内容如下:

<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">

以上标签的 content 属性值,含义依次为:视口宽度,设置为了设备宽度;用户缩放,设置为禁用;初识缩放比例,设置为 1;最大缩放比例,设置为 1;最小缩放比例,设置为 1。

触摸事件句柄

  • 触摸开始(手指接触) ontouchstart

  • 触目移动(手指移动) ontouchmove

  • 触摸结束(手指抬起) ontouchend

触摸特性检测

可以使用 in 操作符,来检测 window 对象是否包含触摸事件 onmovestart 对应的属性,如果包含则返回 true,不包含则返回 false;而后通过返回值来设置要监听的事件,例如:

1
2
3
4
var hasTouch = 'ontouchstart' in window, // 检测是否为触摸设备
tapStart = hasTouch ? 'touchstart' : 'mousedown',
tapMove = hasTouch ? 'touchmove' : 'mousemove',
tapEnd = hasTouch ? 'touchend' : 'mouseup'; // 监听不同设备事件

触摸事件信息

要获取触摸事件信息,需要先获取触摸点;由于触控设备大都支持多点触控,所以事件对象会将触摸点作为一个集合(非数组)返回,但是一般只处理一个触摸点,就是集合的第 0 个对象,即 touchEvent.touches[0]。其他的位置属性基本上和鼠标事件信息类似。

前端系统课程 - 18. Canvas 画板 (1)

鼠标事件句柄

  • 鼠标按下 onmousedown

  • 鼠标移动 onmousemove

  • 鼠标松开 onmouseup

鼠标事件信息

  • screenX/Y 属性代表鼠标在整个屏幕的位置;

  • clientX/Y 属性代表鼠标相对于视口在窗口或文档的位置;

  • 事件对象中的 button 属性代表鼠标上的键位,一般值为 0 表示左键,1 表示中键,2 表示右键。

canvas 标签

由于使用普通元素无法在短的时间内连贯的监听鼠标位置,导致“点”的连接不连贯,所以用普通元素做画板很不理想。

  • <canvas> 标签和 <img> 标签类似,默认是 inline-block 元素,并自带宽和高属性;

  • 如果用 CSS 设置 Canvas 元素的宽高可能会导致图像扭曲,所以要用它自带的宽高属性设置;

  • 使用 Canvas 之前需要先设置上下文,例如:var ctx = canvasElem.getContext('2d') ,将上下文设为平面;

  • 通过 fillStyle 属性设置填充颜色,通过 strokeStyle 属性设置描边颜色,设置颜色要在填充或绘制动作之前;

  • 使用 fillRect() 方法可填充出一个矩形,4 个参数分别为(矩形左上角距画布左上角):X 轴距离,Y 轴距离,宽度,高度;

  • 通过 strokeStyle 属性设置描边颜色;

  • 使用 strokeRect() 方法可绘制出一个矩形,参数同上;

  • 使用 clearRect() 方法将一个矩形区域的内容清除,利用这个方法可以做橡皮擦功能,参数同上;

  • 利用 globalCompositeOperation 属性可以更方便的作出橡皮擦效果,这个属性的默认值是 source-over 意思是在画布已有像素上绘图时会将新像素叠加,而另一个值 destination-out 的作用是在已有像素基础上绘图时,绘制区域的已有像素会被重置为透明,类似刮刮卡效果;利用这个属性还可以设置画布颜色,更多属性可以探索;

  • 使用 arc() 方法可以绘制出一个圆形,6 个参数分别为(圆心距画布左上角):X 轴距离,Y 轴距离,半径,开始弧度,结束弧度(弧度以 π 为 180°),是否为逆时针;

  • 除了矩形,生成其他图形都要以 beginPath() 开始,以 closePath() 结束;

  • 使用 lineWidth 属性可以设置线的粗细;

  • 设置 lineCap = 'round' 可以使线条两端为圆弧;设置 lineJion = 'round' 可以使线条转折为圆弧;

  • 连续画线的关键在于,起始点位置与结束点位置的交替(beginPoint = newPoint);

  • 要让画布全屏,可以将画布宽高分别设置为视口宽高,一般使用 document.documentElement.clientWidth/clientHeight 属性的值,并在窗口改变大小时触发;

  • 使用 save() 方法可以保存当前的画布信息,例如缩放设置,颜色设置等,并不是保存文件;

  • 使用 restore() 方法可以还原上一次用 save() 方法保存的画布信息;

  • 使用 clip() 方法可以将选中区域设置为剪辑区域,所有绘画动作都只能在这个剪辑区域操作;

  • 利用 save()clip() 以及 restore() 方法结合可以制作不同形状橡皮擦效果或刮刮卡效果,例如:

    1
    2
    3
    4
    5
    6
    ctx.save();
    ctx.beginPath();
    ctx.arc(x, y, radius, 0, 2 * Math.PI);
    ctx.clip();
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.restore(); // 这组代码实现了圆形橡皮擦的功能
  • 如果要将 Canvas 画好的图保存为图片,可以使用 canvas.toDataURL() 方法,它会将画布中的图案转变为 base64 编码格式的 PNG 图像并将其 Data URL 返回;方法中可以传入需要保存的文件类型的 MINE 类型参数,一般使用 表示 PNG 格式的 image/png 参数即可。另外,最新的浏览器一般都禁止了在新窗口打开 Data URL,所以需要将 Data URL 保存到一个 <a> 标签的 href 属性中,并给这个标签添加 download 属性设置保存的文件名,最后,触发 <a> 标签的 click() 方法即可打开并保存图片文件。新版火狐中,还需要将这个 <a> 标签添加到页面中才能触发点击事件。