Git Product home page Git Product logo

blog's Introduction

blog's People

Contributors

shiiiiiiji avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

blog's Issues

《JavaScript高级程序设计(第3版)》学习笔记——二、基本概念(ECMAScript 3)

二、基本概念(ECMAScript 3)

对应“js高程ch3”

  • 区分大小写

  • ECMAScript的变量是松散类型的(可以用来保存任何类型的数据),每一个变量仅仅是一个用于保存值的占位符而已。

  • 变量的定义与初始化

    • 声明、定义、初始化、赋值https://www.zhihu.com/question/27639400
    • 声明后未经初始化的变量会保存一个特殊的值undefined
    • 使用var操作符定义的变量将成为定义该变量的作用域中的局部变量
    • 省略var操作符定义并初始化的变量为全局变量(不推荐这种方式)
  • ECMAScript 数据类型

    • 5种简单数据类型(基本数据类型)
      • Undefined
      • Null
      • Boolean
      • Number
      • String
    • 1种复杂数据类型
      • Object
  • ECMAScript 不支持任何创建自定义类型的机制,所有值最终都将是上述6种数据类型之一。

  • typeof操作符(不是函数!!!)

操作数 结果
未定义变量/未初始化变量/Undefined类型值(undefined) "undefined"
Boolean类型值 "boolean"
Number类型值 "number"
String类型值 "string"
Null类型值(null)/Object类型值(除Function) "object"
Function "function"
  • Undefined 类型

    • 只有一个值:undefined
    • vs 定义未初始化变量
      • 使用var声明变量但未对其加以初始化时,变量值即为undefined
      • 没有必要显式初始化变量值为undefined
    • vs 未定义变量
      • 只可进行 typeof / delete 操作(delete无意义,严格模式报错)
      • 技术角度有本质区别,但逻辑角度有合理性
  • Null 类型

    • 只有一个值:null
    • 逻辑上表示一个空对象指针
    • undefined == null // true
    • 只要意在保存对象的变量还没有真正保存对象,就有必要显式初始化变量值为 null
  • Boolean 类型

    • 只有两个字面值:true 和 false
    • 所有其他类型的值都可以通过 Boolean() 方法转换成 Boolean 类型的值
类型 为true值 为false值
Boolean 类型 true false
Undefined 类型 - false
Null 类型 - false
Number 类型 非零数字值(包括无穷大) 0 和 NaN
String 类型 非空字符串 ""(空字符串)
Object 类型 任何对象 -
  • Number 类型

    • IEEE 754 格式:整数和浮点数
    • 浮点数值在进行算数计算时,无法保证其精确度(IEEE 754 数值格式通病)
    • 数值范围
      • Number.MIN_VALUE
      • Number.MAX_VALUE
      • 超出范围的数值将会自动转换成特殊的Infinity值(不能参与计算,值为NaN
        • Infinity(Number.NEGATIVE_INFINITY,正无穷)、-Infinity(Number.POSITIVE_INFINITY,负无穷)
        • isFinite() 方法
    • NaN:非数值(Not a Number)是一个特殊的数值
      • 用于本来返回数值的操作数未返回数值的情况(不会抛出错误导致程序停止)
      • NaN 参与任何计算,都会返回 NaN
      • NaN 与任何值都不相等(包括 NaN 本身)
      • isNaN() 方法在接收到一个参数后会尝试将其转换为数值
    • 数值转换
      • Number() - 用于任何数据类型

        • 如果是 Undefined 类型, undefined -> 0
        • 如果是 Null 类型, null -> 0
        • 如果是 Boolean 类型, true -> 1, false -> 0
        • 如果是 String 类型:
          • 只包含有效数值(包括Infinity和NaN) -> 十进制(八进制前面的0会被忽略,十六进制会转换为十进制)
          • 空字符串 -> 0
          • 包含其他字符 -> NaN
        • 如果是 Object 类型:
          • 首先调用对象的 valueOf() 方法,再按照上述规则
          • 如果转换结果是 NaN, 则调用对象的 toString() 方法,再按照上述规则
      • parseInt() - 用于字符串类型

        • 忽略字符串前面的空格,直至找到第一个非空格字符
          • 如果第一个非空格字符串 -> NaN
          • 一直解析到所有后续字符或者遇到一个非数字字符(包括负号和小数点),然后按照各种整数格式(十进制、八进制、十六进制) -> 十进制
          • 可以指定基数作为第二个参数(此时进制前置符号可忽略)
      • parseFloat() - 用于字符串类型

        • 只解析十进制,无第二个基数参数
        • 结果如果为整数,则会返回整数
        • 第二个小数点无效
  • String 类型

    • ECMAScript 中字符串不可变,一旦创建,值就不能改变。要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个新值的字符串填充该变量。
    • 转换为字符串
      • toString(),除 null 和 undefined 外,所有值都有 toString() 方法,返回相应值的字符串表现
      • 转型函数 String()
        • null -> "null"
        • undefined -> "undefined"
        • toString()
      • +"" // 加字符串
  • Object 类型

    • Object 类型的每个实例(“对象”)都具有下列属性和方法
      • constructor:保存着用于创建当前对象的函数,即构造函数。
      • hasOwnProperty():用来检查给定的属性在当前对象实例中(而不是在对象实例的原型中)是否存在
      • isPrototypeOf():用于检查传入的对象是否是当前对象的原型
      • propertyIsEnumberable():用于检查给定的属性是否能够使用 for-in 语句来枚举
      • toLocalString():返回对象的字符串表示
      • toString():返回对象的字符串表示
      • valueOf():返回对象的字符串、数值或布尔值表示

《JavaScript高级程序设计(第3版)》学习笔记——二、基本概念(ECMAScript 3)

基本概念(ECMAScript 3)

区分大小写。

标识符:指变量、函数、属性的名字,或者函数的参数。命名要求:

  • 第一个字符必须是一个字母、下划线(_)或一个美元符号($)
  • 其他字符可以是字母、下划线、美元符号或数字
    (Question:为什么数字不可以?)

注释:C风格

关键字和保留字

  • 关键字:用于表示控制语句的开始或结束,或者用于执行特定操作等。
  • 保留字:暂时无特定用途,但将来可能用作关键字。
    关键字和保留字均不能用作标识符。

ECMAScript的变量是松散类型的(可以用来保存任何类型的数据),每一个变量仅仅是一个用于保存值的占位符而已。

变量的定义与初始化

  • 声明、定义、初始化、赋值https://www.zhihu.com/question/27639400
  • 声明后未经初始化的变量会保存一个特殊的值undefined
  • 使用var操作符定义的变量将成为定义该变量的作用域中的局部变量
  • 省略var操作符定义并初始化的变量为全局变量(不推荐这种方式)

ECMAScript 数据类型

  • 5种简单数据类型(基本数据类型)
    • Undefined
    • Null
    • Boolean
    • Number
    • String
  • 1种复杂数据类型
    • Object

ECMAScript 不支持任何创建自定义类型的机制,所有值最终都将是上述6种数据类型之一。(ES6新定义了Symbol类型、Set类型、Map类型)

typeof操作符(不是函数!!!)
| 未定义变量/未初始化变量/Undefined类型值(undefined) | "undefined" |
| Boolean类型值 | "boolean" |
| Number类型值 | "number" |
| String类型值 | "string" |
| Null类型值(null)/Object类型值(除Function) | "object" |
| Function | "function" |

Undefined 类型

  • 只有一个值:undefined
  • 未定义变量 vs 定义未初始化变量
    未定义变量:
  • 只可进行 typeof / delete 操作(delete无意义,严格模式报错)
  • 技术角度有本质区别,但逻辑角度有合理性
    定义未初始化变量
  • 使用var声明变量但未对其加以初始化时,变量值即为undefined
  • 没有必要显式初始化变量值为undefined

Null 类型

  • 只有一个值:null
  • 逻辑上表示一个空对象指针
  • undefined == null // true
  • 只要意在保存对象的变量还没有真正保存对象,就有必要显式初始化变量值为 null

Boolean 类型

  • 只有两个字面值:true 和 false
  • 所有其他类型的值都可以通过 Boolean() 方法转换成 Boolean 类型的值
    | Boolean 类型 | true | false |
    | Undefined 类型 | - | false |
    | Null 类型 | - | false |
    | Number 类型 | 非零数字值(包括无穷大) | 0 和 NaN |
    | String 类型 | 非空字符串 | ""(空字符串) |
    | Object 类型 | 任何对象 | - |

Number 类型

  • IEEE 754 格式:整数和浮点数
  • 浮点数值在进行算数计算时,无法保证其精确度(IEEE 754 数值格式通病)
  • 数值范围
    • Number.MIN_VALUE
    • Number.MAX_VALUE
    • 超出范围的数值将会自动转换成特殊的Infinity值(不能参与计算,值为NaN
    • Infinity(Number.NEGATIVE_INFINITY,正无穷)、-Infinity(Number.POSITIVE_INFINITY,负无穷)
    • isFinite() 方法
  • NaN:非数值(Not a Number)是一个特殊的数值
    • 用于本来返回数值的操作数未返回数值的情况(不会抛出错误导致程序停止)
    • NaN 参与任何计算,都会返回 NaN
    • NaN 与任何值都不相等(包括 NaN 本身)
    • isNaN() 方法在接收到一个参数后会尝试将其转换为数值
  • 数值转换
    • Number() - 用于任何数据类型

      • 如果是 Undefined 类型, undefined -> 0
      • 如果是 Null 类型, null -> 0
      • 如果是 Boolean 类型, true -> 1, false -> 0
      • 如果是 String 类型:
        • 只包含有效数值(包括Infinity和NaN) -> 十进制(八进制前面的0会被忽略,十六进制会转换为十进制)
        • 空字符串 -> 0
        • 包含其他字符 -> NaN
      • 如果是 Object 类型:
        • 首先调用对象的 valueOf() 方法,再按照上述规则
        • 如果转换结果是 NaN, 则调用对象的 toString() 方法,再按照上述规则
    • parseInt() - 用于字符串类型

      • 忽略字符串前面的空格,直至找到第一个非空格字符
      • 如果第一个非空格字符串 -> NaN
      • 一直解析到所有后续字符或者遇到一个非数字字符(包括负号和小数点),然后按照各种整数格式(十进制、八进制、十六进制) -> 十进制
      • 可以指定基数作为第二个参数(此时进制前置符号可忽略)
    • parseFloat() - 用于字符串类型

      • 只解析十进制,无第二个基数参数
      • 结果如果为整数,则会返回整数
      • 第二个小数点无效

String 类型

  • ECMAScript 中字符串不可变,一旦创建,值就不能改变。要改变某个变量保存的字符串,首先要销毁原来的字符串,然后再用另一个新值的字符串填充该变量。
  • 转换为字符串
    • toString(),除 null 和 undefined 外,所有值都有 toString() 方法,返回相应值的字符串表现
    • 转型函数 String()
      • null -> "null"
      • undefined -> "undefined"
      • toString()
    • +"" // 加字符串

Object 类型

  • Object 类型的每个实例(“对象”)都具有下列属性和方法(均存在于Object.prototype
    • constructor:保存着用于创建当前对象的函数,即构造函数。
    • hasOwnProperty():用来检查给定的属性在当前对象实例中(而不是在对象实例的原型中)是否存在
    • isPrototypeOf():用于检查传入的对象是否是当前对象的原型
    • propertyIsEnumberable():用于检查给定的属性是否能够使用 for-in 语句来枚举
    • toLocaleString():返回对象的字符串表示
    • toString():返回对象的字符串表示
    • valueOf():返回对象的字符串、数值或布尔值表示

解析查询字符串函数封装实现

js高程(第三版)实现源码

function getQueryStringArgs(){
	// 取得查询字符串并去掉开头的问号
	var qs = (location.search.length > 0 ? location.search.substring(1) : ""),

		// 保存数据的对象
		args = {},

		// 取得每一项
		items = qs.length ? qs.split("&") : [],
		item = null,
		name = null,
		value = null,

		// 在 for 循环中使用
		i = 0,
		len = items.length;

	// 逐个将每一项添加到 args 对象中
	for(i = 0; i < len; i++) {
		item = items[i].split("=");
		name = decodeURIComponent(item[0]);
		value = decodeURIComponent(item[1]);

		if(name.length){
			args[name] = value;
		}
	}

	return args;
}

《JavaScript高级程序设计(第3版)》学习笔记——十一、表单脚本

表单脚本

JavaScript 最初的一个应用,就是分担服务器处理表单的职责,打破处处依赖服务器的局面。

表单基础知识

  • html —— <form>元素
  • JavaScript —— HTMLFormElement 类型对象

提交表单:

  • 用户单机提交按钮或图像按钮时,就会提交表单
<!-- generic submit button -->
<input type="submit" value="Submit Form">
<!-- custom submit button -->
<button type="submit">Submit Form</button>
<!-- image button -->
<input type="image" src="graphic.gif">

以这种方式提交表单时,浏览器会在将请求发送给服务器之前触发 submit 事件。 => 我们有机会验证表单数据,据决定是否允许表单提交(阻止这个事件的默认行为就可以取消表单提交)

var form = document.getElementById("myForm");
	EventUtil.addHandler(form, "submit", function(event){
	// 取得事件对象
	event = EventUtil.getEvent(event);
	// 阻止默认事件
	EventUtil.preventDefault(event);
});
  • 在 JavaScript 中,以编程方式调用 submit() 方法也可以提交表单
    而且,这种方式无需表单包含提交按钮,任何时候都可以提交。但调用 submit() 方法不会触发 submit 事件,因此在调用之前要先验证表单数据

重复提交表单:

  • 第一次提交表单后,就禁用提交按钮
  • 利用 onsubmit 事件处理程序取消后续的表单提交操作

重置表单

<!-- generic reset button -->
<input type="reset" value="Reset Form">
<!-- custom reset button -->
<button type="reset">Reset Form</button>
var form = document.getElementById(“myForm”);
//reset the form
form.reset();

与调用 submit() 方法不同,调用 reset() 方法会像单击重置按钮一样触发 reset 事件。

表单字段

  • 共有的表单字段属性
  • 共有的表单字段方法
  • 共有的表单字段事件

文本框脚本

  • 选择文本:
    • select() 方法
    • setSelectionRange() 方法
    • 选择(select)事件
  • 过滤输入
    • 屏蔽字符
     EventUtil.addHandler(textbox, "keypress", function(event){
     	event = EventUtil.getEvent(event);
     	var target = EventUtil.getTarget(event);
     	var charCode = EventUtil.getCharCode(event);
     	if (!/\d/.test(String.fromCharCode(charCode)) && charCode > 9 && !event.ctrlKey){
     		EventUtil.preventDefault(event);
     	}
     });
    • 操作剪贴板
    • 自动切换焦点
    • HTML 5 约束验证 API(原生支持,无需 JavaScript)
      • 必填字段:required
      • 其他输入类型:email、url
      • 数值范围
      • 输入模式:pattern
      • 检测有效性:checkValidity()
      • 禁用验证:novalidate

选择框脚本

表单序列化

富文本编辑

又称为 WYSIWYG(What You See Is What You Get,所见即所得)

使用 contenteditable 属性

《JavaScript高级程序设计(第3版)》学习笔记——十三、HTML5脚本编程

HTML 5 脚本编程

跨文档消息传递

cross-document messaging

XDM

postMessage() 方法与 window 上的 message 事件

不降低同源策略安全性的前提下,在来自不同域的文档间传递消息

原生拖放

  • dragstart
  • drag
  • dragend

实现一个拖拽 demo

媒体元素

  • audio
  • video

历史状态管理

hashChange 事件,可以知道 URL 的参数什么时候发生了变化。

通过状态管理 API,能够在不加载页面的情况下改变浏览器的URL。=> history.pushState(),接收三个参数:状态对象、新状态的标题和可选的相对 URL。

history.pushState({name: 'Nicholas'}, 'Nicholas page', 'nicholas.html');

执行 pushState() 方法后,新的状态信息就会被加入历史状态栈,而浏览器地址栏也会变成新的相对 URL

但是,浏览器并不会真的向浏览器发送请求,即使状态改变之后查询 location.href 也会返回与地址栏中相同的地址。

且,pushState() 会创建新的历史状态,按下“后退”,会触发 window 对象的 popState 事件(其中的 state 属性,包含这当初以第一参数传递给 pushState 的状态对象)

得到这个状态对象后,必须把页面重置为状态对象中的数据表示的状态(浏览器不会自动做这些)。浏览器加载的第一个页面没有状态,因此单击“后退”按钮返回浏览器加载的第一个页面时,event.state 值为 null

要更新当前状态,可以调用 replaceState(),调用这个方法不会在历史状态栈中创建新状态,只会重写当前状态。

历史状态管理可以让我们不必卸载当前页面即可修改浏览器的历史状态栈。有了这种机制,用户就可以通过“后退”和“前进”按钮在页面状态间切换,而这些状态完全由 JavaScript 进行控制。

iOS 下 iframe 撑开父容器高度 bug

最近在调试一个非常诡异的 bug ,在 PC 和安卓下都是正常的,但是发现在 iOS 却出现问题。代码如下:

  • HTML
<div class="wrapper">
	<iframe src="https://www.cnblogs.com/DSC1991/p/8665891.html"></iframe>
</div>
  • CSS
.wrapper{
  width: 100%;
  height: 200px;
  
}

.wrapper iframe{

}

image

发现iframe竟然不受父容器的高度限制,撑开了父容器,导致滑动UI显示异常。

在父容器应用下面CSS便显示正常:

-webkit-overflow-scrolling: touch;
overflow-y: scroll;

参考

Win 7 下安装 node-sass 失败解决方法

本机环境:Window 7

其实因为antd-mobile依赖了node-sassnode-gyp包,安装 node-sass 的正确姿势,正确安装后一般都没什么问题。

但是在本机还遇到一下问题,将解决方法记录下来:

1、缺少Python
image

解决方案:安装Python后执行如下命令(替换为自己电脑的路径):

npm config set python C:\Programs\Python27\python.exe

2、
image

解决方案:下载.Net Framework后安装后重启电脑生效。(仅安装这个没用)
解决方案:参照npm install socket.io 提示缺少“VCBuild.exe”,一定要装VS C++吗?

看答案里有一个比较简单的方法是:
nodejs/node-gyp#307 (comment)

npm install --global --production windows-build-tools

3、
image

看到报错觉得可能是node_modules的问题,然后就;

rm -rf node_modules

成功啦!不容易~

image

MAC大法好!

组件库通用样式设计总结

前言

作为前端UI组件库,从样式角度去看,应当满足两方面要求:一致性可定制[1]

其实这两点也非常好理解,一致性保证了组件库视觉上保持一致,而不是东拼西凑,而且说得高大上一点可能还有规范可循。而可定制就需要组件库暴露接口,供开发者配置形成自己风格的组件库。

一致性

但是具体一致表现在哪些方面呢?对于设计师而言,会很清楚,但是对于我们前端开发人员而言,具体指的是哪些东西呢?我们又如何把这些东西转化为代码呢?这部分具体见设计规范部分。

可定制

根据可定制的粒度大小,可以分为组件层面的可定制和整套组件库的主题定制。有组件使用经验的同学都知道,使用具体组件时我们可以传入某些参数或主题参数,组件就可以呈现不同的表现。另外,一些有名的组件库也都提供了主题定制的相关方法,如 antd-mobile 、Vant 和 Element ,尤其是 Element ,提供了多种主题定制的方法。

设计规范先行

前面提到的一致性是由“设计规范”来保证的,其实这一块涉及到的内容非常多。可能在我们眼里就是组件库里的那套看似杂乱无序通用变量(设计规范 ≠ 通用变量),但是其实里面还是有一些套路的,也建议多多和设计师沟通,产生思维碰撞,你会发现一些平时写代码过程中不一样的思维。在我和设计师沟通的过程中,这点体验非常深。

设计规范包含哪些内容?

那和组件库密切相关的设计规范体现在哪些方面呢?前几天饿了么前端架构师 林溪 在《Vue组件库建设实践》分享里提到有以下内容:

  • 统一视觉样式
    • 色彩
    • 布局
    • 字体
    • 图标
  • 统一交互动效
    • 时长、缓动
    • 移动路径
    • 形变、编排

设计规范如何落地?

其实上面有一个关键词: 统一 。 我们如何保证样式统一?没错,就是刚才提到的通用变量(但我仍然不会认同“设计规范 = 通用变量”,很难表达出这种感觉),以 antd-mobile 为例,具体包含以下内容[2]

  • 颜色(字体、填充、全局、边框、透明度)
  • 字体尺寸
  • 字体族
  • 圆角
  • 边框尺寸
  • 间距(水平、垂直)
  • 高度
  • 图标尺寸
  • 部分组件样式

其实理解了设计规范,我们再来反过来理解组件库里面的通用变量就会感觉其实变量之间其实也是存在某种内在体系的,在写具体组件的样式时就更会或者更能遵守这套规范,而不是胡乱定义了。

相关扩展资料:

主题定制

主题定制是大多数组件库都会提供的一个核心样式相关的功能。但是具体解决方案有许多,但无非有两种解决方式:

  1. 借助gulp/webpack等打包工具相关的插件,配置需要定制的样式变量,在打包时覆盖对应变量值;
  2. 手动修改通用变量,或者新写一套样式文件覆盖组件库样式。

具体可参照知名组件库做法:

不过,对于企业内部组件库而言,这一块其实要求并不强,相反对于一套组件库提供多套 配色 需求更高一点。这里指的配色和主题定制还是有区别的,因为配色要求不同“皮肤”的样式需要同时存在,而主题定制相当于就只有一套配色。

而如果采用多种配色的话,增加一套配色文件也可以达到“主题定制”的功能。

其他通用样式

除了设计规范里的相关样式外,其实还包含但不限于如下通用/复用样式:

  • reset / normalize
  • hairline
  • animation
  • util / mixins
  • clearfix
  • ……

这里不仅仅组件库,一般项目中也会有这方面的通用样式,相信大家都明白,这里就不展开介绍了。但是这也很重要,是组件库样式基础的一部分!

代码结构

下面是借鉴知名组件库后设计的一个样式代码结构,供参考,供拍砖。

style
├── core
|   ├── animation.scss
|   ├── color-card.scss
|   ├── hairline.scss
|   ├── mixins.scss
|   └── normalize.scss
├── themes
|   ├── skin
|   |   ├── _brown.scss
|   |   ├── _green.scss
|   |   ├── _pink.scss
|   |   ├── ……
|   |   └── _white.scss
|   └── default.scss
├── core.scss
└── index.scss

另外,为了开发方便,组件相关样式变量都放在了组件代码目录,虽然会给后期维护(增加“配色”时需要更新每个组件样式特定代码),但是这种情况甚少。不过通用型组件库会将其写在通用变量中以方便主题定制粒度到组件层面。

展望

还需要考虑的一些样式相关的问题:

  1. Icon
  2. 高清方案
  3. 兼容性(如flex兼容性等)
  4. 按需加载
  5. ……

另外,其实组件库有通用型组件库和业务型组件库区分,一般有名的组件库都属于前者,很多解决方案不一定适合企业内部的业务型组件库,因此在设计时也需要充分考虑业务需要,找到适合自己的最/较优解。

参考


前端晚自修

原文发表在《前端晚自修》:【第18期】组件库通用样式设计总结.md

特殊情况下this的引用总结

首先,对于this的理解,我个人看到有两种说法:

  1. this是函数的一个内部属性,见js高程第三版p113,引用的是(,或者说this的值是)函数据以执行的环境对象,或者说this对象是在运行时基于函数的执行环境绑定的(js高程第三版p182)
  2. this是函数执行上下文的一部分。(执行上下文是在代码执行前(非定义时)会创建的一个环境,环境创建完成之后才会真正开始执行代码,而其中代码里所需要的变量都会优先从当前上下文中查询,除了this和arguments这两个变量只会从当前环境中查找)

常规情况下,我们一般都认为this的引用有如下几种情况:

  • 构造函数作为构造函数执行的时候,其中的this指向的是要返回的实例
  • 作为对象的方法执行时,方法中的this引用的就是该对象
  • 作为普通方法执行时,其中的this引用的是函数所处的环境
  • 使用apply、call、bind等方法可以直接修改函数执行过程中的this值

其实,还有一些特殊情况:

  • 匿名函数执行时,this指向全局(js高程p182,匿名函数的执行环境具有全局性)
    image
  • 超时调用和间歇调用的函数执行时,this指向全局(可以理解为window.setTimeout())
  • 事件处理程序在正常情况下,this指向绑定元素对象的引用

由一个简单的问题重新思考this的引用问题

刚才,群里有一位同学提出这样一个问题:

var x = 1;
var obj = {
	x: 2,
	y: function(){
		console.log(this.x);
	}
}
setTimeout(obj.y, 1000);

这个问题其实是考查的是js的异步执行的问题,不深入的去解释的话,js高程(p203)对超时调用的函数特别强调:

超时调用的代码都是在全局作用域中执行的,因此函数中this的值在非严格模式下指向window对象,在严格模式下指向undefined
image

因此,就认定this指向的是window,因此结果是1,当然正确结果也是1。

这里obj.y不是调用关系,而是赋值,传递给超时函数作为形参。

最开始我却认为这和异步执行没关系,即使直接不用超时函数,obj.y();,这个结果应该也是1(最简单的js代码,啪啪打脸),而且有理有据,obj.y仅仅只是一个函数的名称而已,而且在全局作用域下执行的,因此结果应当是1,甚至群里许多人都被我带偏了,认为我说的对。

但是其实不然,我也不知道为什么我这个解释为什么错了(哈哈哈),obj.y();这个非常常规的对象函数调用结果是2。

首先,函数执行的this值,是指向函数据以执行的环境对象(js高程p114),但是如何确定是哪个环境对象呢?

由此,进一步我提出了下面问题,怎么解释下面4段代码:

(function(){return obj.y})()();  // 1
var c = obj.y; c(); // 1
(obj.y)();  // 2
(function(){return this.x})();  // 1

《JavaScript高级程序设计(第3版)》学习笔记——七、BOM

BOM

The Browser Object Model,浏览器对象模型

window 对象

BOM 核心对象是 window ,表示浏览器的一个实例。

  • 既是通过 JavaScript 访问浏览器窗口的一个接口
  • 又是 ECMAScript 规定的 Global 对象

全局作用域

所有在全局作用域中声明的变量、函数都会变成 window 对象的属性和方法。

定义全局变量 vs 在 window 对象上定义属性

  • 全局变量不能通过 delete 操作符删除。而直接定义在 window 对象上定义的属性可以。(因为前者定义属性的[[Configurable]]特性值为false)
  • 尝试访问未声明的变量会抛出错误,但是通过查询 window 对象,可以知道某个可能未声明的变量是否存在

窗口关系及框架

如果页面中包含框架,则每个框架都有自己的 window 对象。并且保存在 frames 集合中。

  • top 对象:始终指向最高(最外)层框架,也就是浏览器窗口
  • parent 对象:始终指向当前框架的直接上层框架
  • self 对象:始终指向 window

需要注意的是,在使用框架的情况下,浏览器中会存在多个Global对象,在每个框架中定义的全局变量会自动成为该框架中 window 对象的属性。由于每个 window 对象都包含原生类型的构造函数,因此每个框架都有一套自己的构造函数,这些构造函数一一对应,但并不相等。=>这个问题会影响到对跨框架传递的对象使用 instanceof 操作符。

窗口位置

确定和修改 window 对象位置的属性和方法。(跨浏览器需要做好兼容性)

  • window.screenLeft / window.screenX
  • window.screenTop / window.screenY
  • window.moveTo()
  • window.moveBy()
var leftPos = (typeof window.screenLeft == 'number') ? window.screenLeft : window.screenX;
var topPos  = (typeof window.screenTop == 'number')  ? window.screenTop  : window.screenY;

窗口大小

跨浏览器确定窗口大小同样不简单。

  • window.innerWidth

  • window.innerHeight

  • window.outerWidth

  • window.outerHeight

  • 浏览器窗口本身尺寸

  • 页面视图容器尺寸

  • 视口(viewport)大小

  • (window.)document.documentElement.clientWidth

  • (window.)document.documentElement.clientHeight

  • (window.)document.body.clientWidth

  • (window.)document.body.clientHeight

对于移动设备,window.innerWidth 和 window.innerHeight 保存着可见视口,也就是屏幕上可见页面区域的大小。(移动 IE 浏览器不支持,但可以通过 document.documentElement.clientWidth 和 document.documentElement.clientHeight 提供相同的信息。)随着页面的缩放,这些值也会相应变化。

在其他移动浏览器中,document.documentElement 度量的是布局视口,即渲染后页面的实际大小(与可见视口不同,可见视口只是整个页面中的一小部分)。移动 IE 浏览器把布局视口的信息保存在 document.body.clientWidth 和 document.body.clientHeight 中。这些值不会随着页面缩放变化。

https://quirksmode.org/mobile/viewports2.html

  • window.resizeTo()
  • window.resizeBy()

导航和打开窗口

指向新窗口的引用 = window.open(url, name, features, replace)

间歇调用和超时调用

JavaScript 是单线程语言,但它允许通过设置超时值和间歇时间值来调度代码在特定的时刻执行。

超时调用

window.setTimeout(),接受两个参数:

  • 要执行的代码(可以是包含js代码的字符串,但会有性能损失)
  • 以毫秒表示的时间(即在执行代码前需要等待多少毫秒),但经过该时间后指定代码不一定执行

原因如下:

  • JavaScript 是一个单线程序的解释器,因此一定时间内只能执行一段代码。
  • 为了控制要执行的代码,就有一个 JavaScript 任务队列。
  • 这些任务会按照将它们添加到队列的顺序执行。setTimeout() 的第二个参数告诉 JavaScript 再过多长时间把当前任务添加到队列中。
  • 如果队列是空的,那么添加的代码会立即执行;如果队列不是空的,那么它就要等前面的代码执行完了以后再执行。

调用 setTimeout() 之后,会返回一个数值 ID,表示超时调用。这个超时调用 ID 是计划执行代码的唯一标识符,可以用于取消超时调用。要取消尚未执行的超时调用计划,可以调用clearTimeout()方法并将相应的超时调用 ID 作为参数传递给它。

超时调用的代码都是在全局作用域下执行的,因此函数中 this 的值在非严格模式下指向 window 对象,在严格模式下是 undefined。

间歇调用

间歇调用与超时调用类似,按照指定的时间间隔重复执行代码,直至间歇调用被取消或者页面被卸载。

window.setInterval(),接受参数与 setTimeout() 相同。同样,调用 setInterval() 方法会返回一个间歇调用 ID ,可用于在将来某个时刻取消间歇调用,可以调用clearInterval()方法并将相应的间歇调用 ID 作为参数传递给它。

var num = 0;
var max = 10;
var intervalId = null;
function incrementNumber() {
    num++;
    // 如果执行次数达到了 max 设定的值,则取消后续尚未执行的调用
    if (num == max) {
        clearInterval(intervalId);
        alert("Done");
    }
}
intervalId = setInterval(incrementNumber, 500);

取消间歇调用的重要性要>>>高于取消超时调用,因为在不加干涉的情况下,间歇调用将会一直执行到页面卸载。

使用超时调用来模拟间歇调用是一种最佳模式。(在开发环境下,很少使用真正的间歇调用,原因是后一个间歇调用可能会在前一个间歇调用结束之前启动)

var num = 0;
var max = 10;
function incrementNumber() {
    num++;
    // 如果执行次数未达到 max 设定的值,则设置另一次超时调用
    if (num < max) {
        setTimeout(incrementNumber, 500);
    } else {
        alert("Done");
    }
}
setTimeout(incrementNumber, 500);

系统对话框

  • window.alert()
  • window.confirm()
  • window.prompt()

通过这几个方法打开的对话框都是同步和模态的,即显示这些对话框时代码会停止执行,而关掉这些对话框代码又会恢复执行。

两个异步显示的对话框:

  • window.print():显示“打印”对话框
  • window.find():显示“查找”对话框

什么是模态框

location 对象

提供了与当前窗口中加载的文档有关的信息,还提供了一些导航功能。事实上,loaction 对象是很特别的一个对象,既是 window 对象的属性,也是 document 对象的属性=> window.location 和 document.location 引用的是同一个对象。

window.location === document.location; // true

location 对象的用处不只表现在它保存着当前文档的信息,还表现在它将 URL 解析为独立的的片段。

查询字符串参数

function getQueryStringArgs(){
    // 取得查询字符串并去掉开头的问号
    var qs = (location.search.length > 0 ? location.search.substring(1) : ''),
        args = {},  // 保存数据的对象
        items = qs.length ? qs.split('&') : [], // 取得每一项
        item = null,
        name = null,
        value = null,
        // 在 for 循环中使用
        i = 0, 
        len = items.length;
    
    for(i = 0; i < len; i++){
        item = items[i].split('=');
        name = decodeURIComponent(item[0]);
        value = decodeURIComponent(item[1]);
        if(name.length){
            args[name] = value;
        }
    }

    return args;
}

位置操作

  • location.assign()
    立即打开新URL,并在浏览器的历史记录中生成一条记录。location.href 或 window.location 设置为一个URL值,也会以该值调用 assign() 方法。

修改 location 对象的其他属性也可以改变当前加载的页面。每次修改 location 的属性(hash除外),页面都会以新 URL 重新加载。(都会在浏览器的历史记录中生成一条新纪录,用户单击“后退”按钮都会导航到前一个页面)

  • location.replace()
    不会在历史记录中生成新记录。

  • location.reload()
    重新加载当前显示的页面。如果调用 reload() 时不传递任何参数,页面就会以最有效的方式重新加载,也就是说,如果页面自上次请求以来并没有改变过,页面就会从浏览器缓存中重新加载。如果要强制从服务器重新加载,需要传递true,location.reload(true)

位于 reload() 之后的代码可能会也可能不会执行,取决于网络延迟或系统资源等因素,所以最好将**reload()**放在代码的最后一行。

location.reload();      // 重新加载(有可能从缓存中加载)
location.reload(true);  // 重新加载(从服务器重新加载)

navigator 对象

识别客户端浏览器的事实标准。navigator.userAgents

screen 对象

保存着与客户端显示器有关的信息,一般只用于站点分析

history 对象

保存着用户上网的历史记录。

每个浏览器窗口、每个标签页乃至每个框架,都有自己的 hostory 对象与特定的 window 对象关联。

处于安全考虑,开发人员无法直接获取用户浏览的 URL ,但借由用户访问过的页面列表,同样可以在不知道实际 URL 的情况下实现后退和前进。

  • history.go()
  • history.back()
  • history.forward()
  • history.length

图片自适应宽度且保持长宽比的实现方法

在业务过程中,经常会收到UI这样的要求:

UI:这里的banner图后台会配置好,我们给他们固定尺寸(比例)的图片,你这里要根据保证宽度自适应,而且长宽比要保持750*180的比例。
FE:好的,没问题

可是,在实现过程中,如果设置:

img{
  width: 100%;
  height: ?
}

高度如何设置呢,24%?——不对呀,高度一般是参照父容器元素的height,宽度虽然自适应了,但是怎么让高度是参照宽度的比例来设置呀?

后来查资料过程中发现,padding属性如果给定百分比值时,是参照包含容器宽度的

The size of the padding as a percentage, relative to the width of the containing block.
参见https://developer.mozilla.org/en-US/docs/Web/CSS/padding

就可以以下面方式来实现:

.banner-item{
  width: 100%;
  height: 0;
  padding-bottom: 24%;
  background: url('https://dummyimage.com/750x180/ddd/fff') no-repeat center;
}

支付宝小程序内实现瀑布加载(上拉、下拉以及异常情况处理)总结

这篇写的比较细,主要是针对同类业务一个比较细的总结,肯定有更好的实现方案,而且也有很多需要改进的地方。

瀑布式加载(也称分页加载)是移动端页面一种比较常见的列表呈现方式,之前大致总结过在实现过程中需要注意和考虑的一些点:前端实现瀑布流列表考虑点总结

简单说来,即初始加载第一页数据,当用户往上滑动屏幕时,出现“正在加载中”文案,并异步请求第二页数据,请求完成后隐藏“正在加载中”文案,继续滑动时如果发现所有数据全部请求完了,就显示“亲,就这么多了”,并且用户再上滑时不会再触发请求。同理,当用户滑到列表最顶部时,出现“下拉刷新”文案,拉取最新数据,覆盖或拼接到页面最顶部。

具体下来,其实里面有很多细节需要考虑:

  • 是全局列表还是局部列表(即列表区域滚动时其他区域是否带动页面其他区域滚动)
  • 第一页列表是否需要异步请求,如果需要异步请求时是否和上拉时的文案“正在加载中”保持一致,即效果类似于上拉,只不过初始没数据而已
  • 初始数据未满一屏,最底部是否显示“亲,就这么多了”
  • 当上拉或者点击另外tab时(多tab列表),是否初始化列表显示“正在加载中”文案
  • 当网络异常情况下,如何控制页面的UI,一直loading,还是以体验更加好的方式呈现给用户。如下图:
    image

对于细节点来说,其实可以理解为列表处于某种状态,而在不同的状态下每一块的UI展示和以及状态间的逻辑维护是比较麻烦的,在使用jQuery或者React实现时,因为对页面的控制权比较大,可以借助在页面上获取的信息能够比较直观和直接维护,但是在小程序环境下,需要完全从数据的角度去维护页面的状态,是比较复杂的

在支付宝小程序下,用到的一些组件和方法如下:

  • scroll-view组件:滚动容器,提供容器滚动时的一些信息和事件(onScrollToLower-滚动到底部/右边,会触发 scrolltolower 事件),但需要注意的是,前提必须给容器一个**“固定”的高度(上下滑)或宽度(左右滑)(至于为什么在这里打引号,是因为我们可以利用flex布局,给一个自适应的“固定”高度**)
  • 因为支付宝小程序不支持组件开发,仅提供UI层面的模板功能,所以可以把“正在加载中”、“亲,就这么多了”、“无结果页面”、“异常页面”封装成一个template,但是逻辑还是需要写在页面的js里,因此复用性以及可维护性极差
  • 状态如何明确:
    • 如何确定用户拉到或快拉到了底部:通过scroll-view的 scrolltolower 事件
    • 因为“亲,就这么多了”,在列表未满一屏时不要显示,如何确定列表满了一屏:通过scroll-view的scroll事件,只要页面滚动了,就能说明页面已满一屏
    • 是否有下一页:每一次异步请求服务端都会返回此状态,或者根据返回列表的长度与pageSize比较
    • 是否在加载中:只要发出请求就手动设置为加载中状态,直至请求成功或失败,否则用户一直触发上拉操作时会发出多个请求,造成状态混乱。
    • 是否断网:相较于H5,小程序拥有更强大的原生系统能力,比较容易获取到系统设置的联网状态,我们在app层设置一个状态,在具体page中就可以在需要判断网络状况时实时获取最新的联网状态。

另外,当列表中某一项的图片加载失败时,如何将其设置为默认图片:这在H5上是很好做到的,我们只要在每一个img标签上绑定onerror事件,触发时将当前img标签的src属性修改为默认图片即可。但在小程序中是不存在DOM概念或者说很难操作页面上的DOM(亲测,查询DOM特别慢),而必须通过data(类似于react中的state)来管理,但是页面列表是不固定的,当触发onerror时如何修改相应的data看起来是不可能完成的。后来换了个思路,设定一个空的图片加载异常状态数组,由其和真实的listData共同来确定每一个image组件的src属性,一旦触发onerror事件,就给数组push一个指定条目(一般是id或者code)的标记,这样就能够准确替换出错的图片了。

《JavaScript高级程序设计(第3版)》学习笔记——六、函数表达式

函数表达式

函数声明的一个重要特征就是函数声明提升(function declaration hoisting),执行代码之前会先读取函数声明。

函数表达式常见的语法形式:创建一个(匿名)函数并将其赋给变量

var fn = function(){
    // 函数体
}

匿名函数(拉姆达函数),匿名函数的name属性是空字符串。

(function(){}).name === '';
var fn = function(){};
fn.name === 'fn';

函数可作为值赋给变量。

递归

递归函数是在一个函数通过名字调用自身的情况下构成的。

arguments.callee —— 指向正在执行的函数的指针(严格模式会报错)

命名函数表达式。

var factorial = (function f(num){
    if(num < 1){
        return 1;
    }else{
        return num * f(num-1);
    }
});

闭包

闭包是指有权访问另一个函数作用域中的变量的函数。——《js高程》
Closures are functions that have access to variables from another function’s scope.
闭包是函数和声明该函数的词法环境的组合。——《MDN》
A closure is the combination of a function and the lexical environment within which that function was declared.


JS变量作用域:使用的是静态作用域。静态作用域在编译阶段就能确定变量的引用。和程序定义的位置有关,和代码执行的顺序无关。(词法作用域,也叫静态作用域,它的作用域是指在词法分析阶段就确定了,不会改变。动态作用域是在运行时根据程序的流程信息来动态确定的,而不是在写代码时进行静态确定的。词法作用域中使用的域,是变量在代码中声明的位置所决定的。)

JS使用词法环境来管理静态作用域。什么是词法环境?=>简单说,就是描述环境的一个对象。那如何描述每个环境呢?

  • 环境记录:用来记录环境里面定义的形参、函数声明、变量等。
  • 对外部词法环境的引用(outer)

词法环境 vs 执行环境(js高程)?应该是一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象。环境中定义的所有变量和函数都保存在这个对象中(虽然代码中无法访问,但在处理数据时解析器会使用到)

全局执行环境是最外围的一个执行环境。根据ECMAScript实现所在的宿主环境不同,表示执行环境的对象也不一样。=>在Web浏览器中,全局执行环境被认为是window对象,因此所有全局变量和函数都是作为window对象的属性和方法创建的。

某个执行环境中的所有代码执行完毕后,该环节被销毁,保存在其中的所有变量和函数定义也随之销毁,全局执行环境直到应用程序退出——例如关闭网页/浏览器时才会被销毁

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。而在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript程序中的执行流正由这个方便的机制控制着。

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象作为变量对象。活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链的下一个变量对象来自包含(外部)环境,层层延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。

标识符解析是沿着作用域链一级一级地搜索标识符的过程。始终从作用域链的前端开始,然后逐级往后回溯,直到找到为止,如果找不到,通常会报错。

var color = "blue";
function changeColor(){
    var anotherColor = "red";
    function swapColors(){
        var tempColor = anotherColor;
        anotherColor = color;
        color = tempColor;
        //color, anotherColor, and tempColor are all accessible here
    }
    //color and anotherColor are accessible here, but not tempColor
    swapColors();
}
//only color is accessible here
changeColor();

内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量和函数。

这些环境之间的联系是线性的、有次序的。每个环境都可以向上搜索作用域链,以查询变量和函数名。但不能向下。

函数参数也被当做变量来对待,其访问规则与执行环境中的其他变量相同。

虽然执行环境的类型总共只有两种:(全局和局部(函数)),但是有一些情况可以延长作用域链:会把指定的对象添加到作用域链前端。

  • try-catch语句的catch块:被抛出的错误对象的声明
  • with:指定的对象

没有块级作用域,即花括号封闭起来的代码块没有自己的作用域,在ECMAScript中就是没有自己的执行环境。如果有块级作用域的话,块级代码执行完毕后,会销毁其变量对象。但在js中没有块级作用域,更没有变量对象(相当于还是在“外部”作用域中)。

执行环境 === 作用域 === 词法环境
作用域链就是将作用域链接起来


JavaScript中的函数会形成闭包。闭包是由函数以及创建该函数的词法环境组合而成。这个环境包含了这个闭包创建时所能访问的所有局部变量。

创建闭包的常用方式,就是在一个函数内部创建另一个函数:

function createComparsionFunction(propertyName){
    return function(object1, object2){

        var value1 = object1[propertyName];
        var value2 = object2[propertyName];

        if(value1 < value2){
            return -1;
        }else if(value1 > value2){
            return 1;
        }else{
            return 0;
        }
    }
}

详细解析如下:即使内部函数(一个匿名函数)被返回了,而且在其他地方调用,但仍然可以访问变量propertyName。之所以还能访问这个变量,是因为内部函数的作用域链中包含 createComparsionFunction 函数的作用域。

这样作用域链难道会分叉吗?不是说是一个栈吗?环境有栈,作用域链为什么没有?=>作用域链是执行环境的,保存在[[Scope]]中。

如何创建作用域链以及作用域链有什么作用。=>彻底理解闭包。

当某一个函数被调用时,会创建一个执行环境及相应的作用域链。然后使用arguments和其他命名参数的值来初始化函数的活动对象(与执行环境对应)。在作用域链中,外部函数的活动对象始终处于第二位,层层回溯直至全局执行环境的变量对象。

在函数执行过程中,为读取和写入变量的值,就需要在作用域链中查找变量。

function compare(v1, v2){
    if(v1 < v2) return -1;
    else if(v1 > v2) return 1;
    else return 0;
}
var res = compare(5, 10);

以上代码先定义了compare函数,然后又在全局作用域中调用了它。

当调用compare函数时,会创建一个包含arguments、v1和v2的活动对象。

全局执行环境的变量对象(包含res和compare)在compare执行环境的作用域链中则处于第二位。如下图所示:执行环境、作用域链、变量对象(活动对象)的关系。

后台的每个执行环境都有一个变量对象。全局环境的变量对象始终存在,而像compare函数这样的局部环境的变量对象,则只有函数执行的过程中存在。

在创建compare函数时,会创建一个预先包含全局变量对象的作用域链,这个作用域链被保存在内部的**[[Scope]]属性**中。

当调用compare函数时,会为函数创建一个执行环境,然后通过复制函数的[[Scope]]属性中的对象构建起执行环境的作用域链。此后,又有一个活动对象(在此作为变量对象使用)被创建并被推入执行环境作用域链的前端。

对于这个例子中compare函数的执行环境而言,其作用域链中包含两个变量对象:本地活动对象和全局变量对象。显然,作用域链本质上是一个指向变量对象的指针列表,它只引用但不实际包含变量对象。

无论什么时候在函数中访问一个变量时,都会从作用域链中搜索具有相应名字的变量。一般来讲,当函数执行完毕后,局部活动对象就会被销毁,内存中仅保存全局作用域(全局执行环境的变量对象)。

但是,闭包的情况有所不同

**在另一个函数内部定义的函数会将包含函数(即外部函数)的活动对象添加到它的作用域链中。**因此,在 createComparsionFunction 函数内部定义的匿名函数的作用域链中,实际上将会包含外部函数 createComparsionFunction 的活动对象。在匿名函数从 createComparsionFunction 中被返回后,它的作用域链被初始化为包含 createComparsionFunction 函数的活动对象和全局变量对象。

实际上,外部函数的活动对象只有在其执行时才会生成,所以闭包如果要包含外部函数的作用域(即其活动对象),外部函数必定执行过。但内部函数的作用域链是在定义时就已经确定了(静态作用域)。

这样,匿名函数就可以访问在 createComparsionFunction 中定义的所有变量。更为重要的是,createComparsionFunction 函数在执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个活动对象。换句话说,当 createComparsionFunction 函数返回后,其执行环境的作用域链会被销毁,但它的活动对象仍然会留在内存中;直至匿名函数被销毁后, createComparsionFunction 函数的活动对象才会被销毁。

// 创建函数
var compareNames = createComparisonFuncion('name');
// 调用函数
var result = compareNames({name: 'zs'},{name: 'ls'});
// 解除对匿名函数的引用(以便释放内存)
compareNames = null;

创建的比较函数被保存在变量compareNames中。而通过将compareNames设置为等于null解除该函数的引用,就等于通知垃圾回收例程将其清除。随着匿名函数的作用域链被销毁,其他作用域(除了全局作用域)也都可以安全的销毁了。

下图展示了调用 compareNames 函数的过程中产生的作用域链之间的关系:

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。主要是由于闭包除了包含自身的作用域链,还会引用包含外部包含函数的作用域,返回后如果不手动解除对其的引用,就会一直占用内存。

闭包与变量

上面提到了标识符解析是沿着作用域链一级一级地搜索标识符的过程。具体到闭包内,保存的是整个变量对象,而不是某个特殊的变量。

一个经典的案例:

function createFunctions(){
    var result = new Array();
    for(var i=0;i<10;i++){
        result[i] = function(){
            return i;
        }
    }
    return result;  // 返回一个函数数组,每个函数的作用域链中都保存着createFunctions()函数的活动对象
}

通过创建另一个匿名函数并理解执行强制让闭包的行为符合预期:

function createFunctions(){
    var result = new Array();
    for(var i=0;i<10;i++){
        result[i] = function(num){
            return function(){
                return num;
            }
        }(i);   // 创建一个自执行匿名函数并将其赋给数组
    }
    return result;
}

由于函数参数是按值传递的,所有就会将变量i的当前值赋值给参数num。

脑洞时刻:如果num是引用类型呢?

function createFunctions(){
    var result = new Array();
    var obj = {};
    for(var i=0;i<10;i++){
        obj[i] = i;
        result[i] = function(num){
            return function(){
                return num;
            }
        }(obj);
    }
    return result;
}

结果并没有达到预期,当在意料之内,因为按值传递时,传的是引用类型值的地址,在闭包里引用的直接上层变量对象虽然不同,但是里面obj变量的值却引用的是同一个地址。

其实,这里说明的一个核心问题在于:闭包,保存的是整个变量对象(的引用),而不是某个特殊的、具体的变量。

关于this对象

上下文是什么意思?上下文 vs 执行上下文?
理解 JavaScript 作用域

this对象是在 运行时 基于函数的执行环境绑定的:

  • 在全局函数中,this等于window。
  • 而当函数被作为某个对象的方法调用时,this等于那个对象
  • 匿名函数的执行环境具有全局性,因此其this对象通常指向window。
  • call、apply、bind

每个函数在被调用时都会自动取得两个特殊的量:thisarguments,内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此永远不可能直接访问外部函数中的这两个变量。(因为活动对象中一定会有这两个变量的定义,即一定会找得到)

不过,把外部作用域中的this对象保存在一个闭包能够访问到的变量里,就可以让闭包访问该对象了。arguments同理,甚至于如果内部作用域和外部作用域变量同名时,都可以用这个方法。

var name = "window";
var object = {
    name: "My Object",
    getName: function(){
        var that = this;
        return function(){
            return that.name;
        }();
    }
}

注意下面三段代码 => 语法的细微变化,都有可能意外改变this的值

object.getName();   // 普通调用
(object.getName)(); // 因为object.getName和(object.getName)的定义是相同的,所有this的值得到了维持(why?)
(object.getName = object.getName)();    // 因为执行了一条赋值语句,然后再调用赋值后的结果。因为这个赋值表达式的值是函数本身,所以this的值不能得到维持(why?)

()这个运算符在js里到底作用是什么?有什么影响吗?

内存泄露

因为闭包会引用包含函数的整个活动对象。不过可以手动对部分变量置为null的方式解除其中内存占用大的变量的内存占用,而取其中有用的部分存储在内存中。

模仿块级作用域/私有作用域

js不会告诉多次声明了同一个变量,这种情况下,后续的声明会忽略,只执行其中的变量初始化。

用作块级作用域(通常也称为私有作用域)的匿名函数的语法表示如下:

(function(){
    // 这里是块级作用域
})();

以上代码定义并立即调用了一个匿名函数。将函数声明包含在一对圆括号中,表示它实际上是一个函数表达式,而紧跟其后的圆括号会立即调用这个函数。

这样定义的私有作用域,会在代码执行结束后被销毁。

通过匿名函数创建的闭包,可以减少闭包占用的内存问题,因为没有指向匿名函数的引用。只要函数执行完毕,就可以立即销毁其作用域链了。

私有变量

在很多编程语言中,可以使用public、private、protected等修饰符来设置成员和方法的访问权限,但在js中,严格来讲没有私有成员的概念,所有对象属性都是公开的。不过,也没有类的概念——构造函数。

不过类似的有私有变量的概念,任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部直接访问这些变量。私有变量包括函数的参数、局部变量和在函数内部定义的其他函数。

不过,我们可以利用闭包创建用于访问私有变量的共有方法。有权访问私有变量和私有函数的共有方法称为特权方法。有如下两种方式创建特权方法:

1、在构造函数内部定义

// 模拟private和public
function MyObject(){
    // 私有变量和私有函数
    var privateVariable = 10;
    function privateFunction(){
        return false;
    }

    // 特权方法
    this.publicMethod = function(){
        privateVariable++;
        return privateFunction();
    }
}

特权方法作为闭包有权访问在构造函数中定义的所有变量和函数。创建MyObject实例后,除了使用publicMethod()这一个途径外,没有任何办法可以直接访问privateVariable和privateFunction。

另外,需要注意的是:函数的内部变量只有当其被调用时才会存在,调用结束一般会立即销毁。但其实本质上每次调用相当于重新创建了一个内部对象,即使上一次函数环境和变量对象未被销毁。因此,上面的函数作为构造函数被调用时,每次调用就相当于创建了一个新的函数执行环境和变量对象,因为返回的闭包被新创建的对象实例所引用导致在调用结束后其变量对象无法被销毁(保存在了内存中,或者叫维护了一个私有作用域,只有这个闭包/公开方法可以访问)。=>私有变量在每一个实例中都不相同,因为构造函数的执行环境会重新创建,更何况是特权方法的执行环境。

使用私有和特权成员,可以隐藏那些不应该被直接修改的数据。

脑洞时间:访问器属性属于创建了一个私有属性吗?(why?)

但使用构造函数模式定义特权方法有一个缺点就是:每次重新调用构造函数都会重新创建构造函数内部定义的方法。

2、通过私有作用域定义
=>静态私有变量。

(function(){
    // 私有变量和私有函数
    var privateVariable = 10;
    function privateFunction(){
        return false;
    }

    MyObject = function(){};    // 没加var,MyObject是全局变量

    // 公有/特权方法
    MyObject.prototype.publicMethod = function(){
        privateVariable++;
        console.log(privateVariable);
        return privateFunction();
    }
})();

这种模式创建了一个私有作用域,在其中,首先定义了私有变量和私有函数,然后定义了构造函数及其公有方法(且公有方法是在原型上定义的)。

需要注意的是,上面直接使用函数表达式定义构造函数,而不是函数声明(会定义在局部环境上),而且没有使用var,因此定义的构造函数是全局环境上的(初始化未经声明的变量,总是会创建一个全局变量)。

其实,上面代码等同于:

var MyObject = function(){};    // 没加var,MyObject是全局变量
// 或者
function MyObject(){};
(function(){
    // 私有变量和私有函数
    var privateVariable = 10;
    function privateFunction(){
        return false;
    }

    // 公有/特权方法
    MyObject.prototype.publicMethod = function(){
        privateVariable++;
        console.log(privateVariable);
        return privateFunction();
    }
})();

这种模式特殊之处,在于私有变量和函数是由实例共享的。且因为特权方法是在原型中定义,因此所有实例都使用同一函数(而特权方法因为在私有作用域内定义的,形成闭包,能够保存着对包含作用域的引用)。

两种方式,完全可以结合使用,视具体需求而定。

另外,对于私有变量,因为多了中间一层作用域,导致查找变量时多查找了一个层次,会在一定程度上影响查找速度。=>闭包和私有变量的一个显明的不足之处。

模块模式

使用闭包可以用于为自定义类型创建私有变量和特权方法,但模块模式则是为单例创建私有变量和特权方法。所谓单例,指的是只有一个实例的对象,而在js中,一般是以对象字面量的方式来创建单例对象的。

???一个实例的对象???对象本身不就是一个实例吗?

var singleton = {
    name: value,
    method: function(){}
}

模块模式通过为单例添加私有变量和特权方法能够使其得到增强。

var singleton = function(){
    // 私有变量和私有函数
    var privateVariable = 10;
    function privateFunction(){
        return false;
    }

    // 特权/公有方法和属性
    return {
        publicProperty: true,
        publicMethod: function(){
            privateVariable++;
            return privateFunction();
        }
    }
}();    // 立即执行匿名函数,返回一个对象

从本质上讲,这个返回的对象字面量定义的是单例的公共接口。这种模式在需要对单例进行某种初始化,同时又需要维护其私有变量时是非常有用的。简言之,如果必须创建一个对象并以某些数据对其进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么就可以使用模块模式。

这种模式创建的单例是Object类型。(因为字面量创建的对象返回)

增强的模块模式

适合那些单例必须是某种类型的实例,同时还必须添加某些属性和(或)方法来对其增强的情况。

var singleton = function(){
    // 私有变量和私有函数
    var privateVariable = 10;
    function privateFunction(){
        return false;
    }
    // 创建对象
    var object = new CustomType();

    // 添加特权/公有属性和方法
    object.publicProperty = true;
    object.publicMethod = function(){
        privateVariable++;
        return privateFunction();
    }

    // 返回这个对象
    return object;
}();

小结

在js中,函数表达式非常有用,使用时无需对函数命名,从而实现动态编程。匿名函数,也成为拉姆达函数。

闭包的作用:

  • 模拟块级作用域
  • 在对象中创建私有变量:可以使用构造函数模式、原型模式来实现自定义类型的特权方法,也可以使用(增强的)模块模式来实现单例的特权方法。

《JavaScript高级程序设计(第3版)》学习笔记——五、面向对象(Object-Oriented)的程序设计

OO

  • 传统OO通过可以创建任意多个具有相同属性和方法的对象
  • 但JavaScript中其实没有类的概念,因此其中的对象也与传统OO中的对象有所不同
  • ECMA-262将对象定义为“无序属性的集合”,散列表:一组名值对,其中值可以是数据或函数
  • 每个对象都是基于一个引用类型创建的(可以是原生类型,也可以是自定义类型) => “类型”和“类”有什么区别?

对象属性的特性(attribute)

描述了属性(property)的各种特征。

  • 属性类型:包括数据属性和访问器属性
    • 数据属性:包含一个数据值的位置,可以读取/写入值。数据属性有4个描述其行为的特性:
      • [[configurable]]:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性
      • [[Enumerable]]:表示能否通过for-in循环返回属性
      • [[Writable]]:表示能否修改属性的值
      • [[Value]]:包含这个属性的数据值。读取属性值时,从这个位置读;写入属性值时,把新值保存在这个位置
    • 访问器属性:不包含数据值。包含一对getter和setter函数(非必需)
      • getter:读取访问器属性时,会调用getter函数,其负责返回有效的值
      • setter:写入访问起属性时,会调用setter函数并传入新值,这个函数决定如何处理数据
      • 访问器属性有如下4个特性:
        • [[configurable]]:表示能否通过delete删除属性从而重新定义属性,能够修改属性的特性,能够把属性修改为数据属性
        • [[Enumerable]]:表示能够通过for-in循环返回属性
        • [[Get]]:在读取属性值时调用的函数
        • [[Set]]:在写入属性值时调用的函数
      • 不一定非要同时指定getter和setter,未指定默认为undefined,未指定即为不可读/不可写
      • 访问器属性不能直接定义
var book = {
    _year: 2004,
    edition: 1
};
Object.defineProperty(book, "year", {
    get: function () {
        return this._year;
    },
    set: function (newValue) {
        if (newValue > 2004) {
            this._year = newValue;
            this.edition += newValue - 2004;
        }
    }
});
book.year = 2005;
alert(book.edition); //2
  • 属性特性的默认值,不同属性定义方式不同
    • 直接定义
      • 数据属性:true/true/true/undefined
      • 访问器属性:<无法直接定义>
    • 属性定义(Object.defineProperty())
      • 数据属性:false(手动设置为false后不可还原,默认时还可以配置)/false/false/undefined
      • 访问器属性:false/false/undefined/undefined
  • 定义属性的特性(可添加并编辑新的属性)
    • Object.defineProperty()
    • Object.defineProperties()
  • 读取属性的特性
    • Object.getOwnPropertyDescriptor()

《JavaScript高级程序设计(第3版)》学习笔记——十二、Canvas

Canvas

画布

基本用法

<canvas id="drawing" width=" 200" height="200">A drawing of something.</canvas>

要在这块画布上绘图,需要取得绘图上下文(对象) => getContext()

var drawing = document.getElementById("drawing");
//􏵆 确定浏览器支持 􏶚􏶛􏱭􏶞􏶟<canvas>􏶆􏶇 元素
if (drawing.getContext){
    var context = drawing.getContext("2d"); // 获得绘图上下文对象
    // ...
}

使用 toDataURL() 方法,可以导出 元素上绘制的对象。

// 取得图像的数据 URI
var imgURI = drawing.toDataURL('image/png');

// 显示图像
var image = document.createElement('img');
image.src = imgURI;
document.body.appendChild(image);

需要注意的是:

  • 默认情况下,浏览器会将图像编码为 PNG 格式
  • 如果绘制到画布上的图像源自不同的域,此方法会报错(跨域)

2D 上下文

使用 2D 绘图上下文提供的方法,可以绘制简单的 2D 图像,如矩形、弧线、路径等。

填充和描边

2D 上下文的坐标开始于 元素的左上角,原点坐标是(0, 0)。基本绘图操作:填充和描边,其结果取决于:

  • fillStyle
  • strokeStyle

属性值可以是字符串、渐变对象或模式对象,默认值“#000000”,设置后所有涉及描边和填充的操作都将使用这两个样式,直至重新设置这两个值。

绘制矩形

矩形是唯一一种可以直接在 2D 上下文中绘制的形状。这三个方法都能接收 4 个参数:矩形的 x 坐标、y 坐标、宽度、高度(单位:像素)

  • fillRect()
  • strokeRect()
  • clearRect()
//􏵿􏵣􏸪􏴀􏷴􏶉 绘制红色矩形
context.fillStyle = "#ff0000"; context.fillRect(10, 10, 50, 50);
//􏵿􏵣􏸫􏸬􏴾 绘制半透明的蓝色矩形
context.fillStyle = "rgba(0,0,255,0.5)"; context.fillRect(30, 30, 50, 50);

//􏵿􏵣􏸪􏴀􏸆􏸇􏷴􏶉 绘制红色描边矩形
context.strokeStyle = "#ff0000";
context.strokeRect(10, 10, 50, 50);
//􏵿􏵣􏸫􏸬􏴾 绘制半透明的蓝色描边矩形
context.strokeStyle = "rgba(0,0,255,0.5)";
context.strokeRect(30, 30, 50, 50);
􏲳􏵉􏸻􏶩􏰲􏳾􏷇􏷴􏶉
context.clearRect(40, 40, 10, 10);
  • 描边线条的宽度:lineWidth
  • 线条末端的形状:lineCap
  • 线条相交的方式:lineJoin

绘制路径

2D 上下文支持很多在画布上绘制路径的方法,通过路径可以创造出复杂的形状和线条。

绘制路径,首先必须调用 beginPath() 方法,表示要开始绘制新路径,然后再通过调用下列方法来实际绘制路径:

  • arc(x, y, radius, startAngle, endAngle, counterclockwise)
  • arcTo(x1, y1, x2, y2, radius)
  • bezierCurveTo(c1x, c1y, c2x, c2y, x, y)
  • lineTo(x, y)
  • moveTo(x, y):绘图游标
  • quadraticCurveTo(cx, cy, x, y)
  • rect(x, y, width, height)

绘制路径后,接下来有几种可能的选择:

  • 如果想绘制一条连接到路径起点的线条,可以调用 closePath()
  • 如果路径已经完成,可以调用 fill() 方法填充(fillStyle)或者 stroke() 方法描边(strokeStyle)
  • 最后还可以调用 clip() 在路径上创建一个剪切区域

在 2D 绘图上下文中,路径是一种主要的绘图方式,因为路径能为要绘制的图形提供更多的控制。由于路径的使用很频繁,可以使用 isPointInPath(x, y) 方法在路径关闭之前确定画布上的某一点是否位于路径上。

if (context.isPointInPath(100, 100)){
    alert("Point (100, 100) is in the path.");
}

绘制时钟表盘(不带数字) demo:

var drawing = document.getElementById(“drawing”);
//make sure <canvas> is completely supported if (drawing.getContext){
var context = drawing.getContext(“2d”);
//start the path
context.beginPath();
//draw outer circle
context.arc(100, 100, 99, 0, 2 * Math.PI, false);
//draw inner circle
context.moveTo(194, 100);
context.arc(100, 100, 94, 0, 2 * Math.PI, false);
//draw minute hand
context.moveTo(100, 100);
context.lineTo(100, 15);
//draw hour hand
context.moveTo(100, 100);
context.lineTo(35, 100);
//stroke the path
context.stroke();
}

绘制文本

  • fillText()
  • strokeText()

接受 4 个参数:要绘制的文本字符串、x 坐标、y 坐标、最大像素宽度(可选)。另外,这两个方法以下列 3 个属性为基础:

  • font
  • textAlign
  • textBaseline

measureText() 方法

绘制图像

drawImage(),注意跨域限制。

CanvasRenderingContext2D.drawImage()

阴影

  • shadowColor
  • shadowOffsetX
  • shadowOffsetY
  • shadowblur

渐变

  • CanvasGradient 实例

模式

模式就是重复的图像,可以用来填充描边图形。


笔者注:渐变和模式都可用来填充描边图形

使用图像数据

getImageData() 取得原始图像数据,返回 ImageData 的实例

合成

  • globalAlpha
  • globalCompositionOperation

WebGL

写在不二书汇设计初

小程序自发布始,外界就一直声称前端开发工程师将大有可为,“替H5、换Android、灭iOS”,口号一个喊得比一个响。作为初入前端的新人,当时早已热血沸腾、心中暗喜,自觉前方一片坦途,正欲摩拳擦掌,干出一番大事业来。

但已一年有余,各类小程序相继发布,心中也曾冒出各种想法和idea,却始终停留在“臆想”阶段,实际却未曾踏出半步(我眼中的「小程序」)。等过了一段时间,便自己找了理由否定掉了。导致到如今,对于小程序的开发文档都还未研究透彻,对于小程序的认识也是人云亦云,没有深刻的认识。

简单列举一下曾经萌发而未实现过的想法:

  • 活动发布
  • 共享同城快递
  • 流程
  • 技术图书漂流
  • 小说明
  • 基于LBS的买卖社群
  • ...

感觉程序员,包括我自己(无冒犯之意),都有一颗“改变世界”的心,自觉无所不能,天生骄傲。见身边不平之事,几句代码便能扭转乾坤,如若不能就自我安慰,投入到无穷的debug之中,对于最初的梦想只能一直搁浅。当然,这只是我对自己长期以来拖延的一种解读,大牛不在之列。

细思原由,眼高手低,基础松散。万丈高楼平地起,总是不能一步登天的。

近期,有两件事算是坚持做了一段时间,感觉不错。一件是17年初参与过21天的“前端早读汇”活动(参与“前端早读汇”有感),一件事前两周坚持了21天的“减肥训练营”。网上流传着一个理论,说21天坚持做同一件事,就能养成一个习惯,可是通过两次实践,感觉并没有真正持续的坐下来,不过如果再多来几个21天不同的实践,真的可能会改变我的心性吧。

自从第一次“前端早读汇”活动,还记得当时就有一个想法在我脑海中一闪而过,就是上面提到的“技术图书漂流”。当时还和@情封沟通过,也和前端早读课的读者群里反馈,大家都觉得不错,从而还成立了杭州的前端早读课分舵。但是因为整件事情一直停留在最初的想法阶段,导致后期根本没办法执行,再加上一些不可控因素,便不了了之。

但历经半年多,这个想法还停留在我脑子里,而且再加上还想好好脚踏实地的去坚持做一件类似于“21天”的事情,这个想法最近一直在我脑海里不停浮现。

具体说来,是因为作为程序员,其实我们都会去买一些纸质技术书,但是因为业务忙加上一有问题就会寻求网络搜索解决方案(当然最新的技术或者第一手文档还是网络资源比较及时),纸质书一直都是我们垫电脑的最佳工具。但是,技术书一般定价也比较高,实属资源浪费。而且,看书这种方式更多也是读书时期多采用的学习方法,一般都会在理论上反复斟酌,再加上老师的讲解和同学的讨论,对于知识的理解也相对比较深入。反观如今,网络发达、资源泛滥的时代,对于很多事物,经常是不求甚解,浮躁,肤浅。

所以,就想通过这么一个形式,来表达一种“格物致知”的态度,对待生活如此,对待技术更是如此。这便是“不二书汇”产生的来由。

“不二书汇”的logo
buer

qrcode_for_gh_d0fc75aa03b6_258

《JavaScript高级程序设计(第3版)》学习笔记

  • 一、JavaScript简介 #2
  • 二、基本概念 #25
  • 三、变量、作用域和内存问题 #21
  • 四、引用类型 #22
  • 五、面向对象(Object-Oriented)的程序设计 #23
  • 六、函数表达式 #35
  • 七、BOM #36
  • 八、客户端检测 #37
  • 九、DOM #38
  • 十、事件 #41
  • 十一、表单脚本 #42
  • 十二、Canvas #46
  • 十三、HTML5脚本编程 #47
  • 十四、错误处理与调试 #48
  • 十五、JSON #49
  • 十六、Ajax #50
  • 十七、高级技巧 #51
  • 十八、离线应用与客户端存储 #52
  • 十九、最佳实践 #53
  • 二十、新兴的API #55

支付宝小程序跳转H5的详细说明

随着小程序的流行,大家都能大概理解“小程序”和传统“程序”之间的差别,其“无需下载”、“近原生体验”等核心能力深受大家追捧。但在使用过程中,其中一些细微的{差别}却不得不需要我们重新去理解和思考小程序的定位。

本文着重讲解小程序提供的“跳转H5”能力。也是最容易被大家误解和滥用的一个功能。

原理

微信小程序和支付宝小程序都提供了“跳转H5”的能力,但是和传统意义上理解的{跳转}并不一致。在传统意义上的跳转,跳转到App或者是跳转H5,跳转过去了可能就和{自己手动打开一个App或者H5}的表现差不多。

但是小程序并非如此,在小程序里跳转H5,本质上是由小程序自身提供的一个叫做<web-view />组件实现的:
支付宝小程序的<web-view />https://docs.alipay.com/mini/component/web-view
微信小程序的<web-view />https://developers.weixin.qq.com/miniprogram/dev/component/web-view.html

我们都知道,对于H5,我们一般都是通过浏览器{在微信和支付宝里打开也是一种特殊的“浏览器”}打开的,虽然有些差异,但是之间都还是传统的浏览器打开。

但是,对于小程序来说,提供的<web-view />组件只是小程序的一部分。虽然也类似“浏览器”,但是并不提供大部分浏览器的一些功能,而且更受到小程序的一些限制

打开的H5,仍然还是在小程序的内部,从打开H5的那一刻起,包括在H5内部的点击跳转,都仅仅被认为是小程序的一个页面而已。点击左上角返回,是返回到小程序的上一级页面

限制

因为实现原理不同,所以在小程序内“跳转H5”还是存在很多限制的,以“支付宝小程序”为例,特此总结如下几条供参考:

  • 打开的H5的域名需要预先在小程序管理后台进行配置,目前已知最多配置20个
  • 跳转到“生活号”链接需要使用特定的API,而且需要是和小程序同主体下
    webview目前不支持一切alipay.com的域名跳转,包括但不限于:支付宝平台提供的授权页面(即H5弹出的授权页面)
  • 无法实现类似医疗健康首页沉浸状态栏式的头部
  • H5原先使用的部分JSAPI功能无法使用,包括pushWindow跳转等等
  • 尽量不要打开含独立授权体系的页面,否则打开一个H5,就要登录或者授权(有可能依赖的获取授权方式在小程序内无法支持),体验非常不好
  • 后续继续补充

建议

综上,其实小程序对于H5的限制还是非常大的,而且小程序和H5之前通信并不友好。

从技术角度上来看,小程序提供的跳转H5功能,目前来看适用于一些展示型的静态的H5页面,否则,在使用过程中可能会出现很多意想不到的bug。

《JavaScript高级程序设计(第3版)》学习笔记——八、客户端检测

客户端检测

能力检测(又称特性检测):目标不是识别特定的浏览器,而是识别浏览器的能力。

// 能力检测基本模式
if(object.propertyInQuestion){
    // 使用 object.propertyInQuestion
}

更严谨的能力检测:typeof

function isSortable(object){
    return typeof object.sort == "function";
}

怪癖检测:识别浏览器的特殊行为

用户代理检测:检测用户代理字符串来确定实际使用的浏览器。在每一次 HTTP 请求过程中,用户代理字符串是作为响应首部发送的,而且该字符串可以通过 JavaScript 的 navigator.userAgent 属性访问。

用户代理字符串的历史:HTTP 规范(包括1.0和1.1版)明确规定,浏览器应该发送简短的用户代理字符串,指明浏览器的名称和版本号。

引入自定义字体时webpack打包报错

今天基于iconfont封装Icon组件的时候,当引入字体样式后,webpack一直报错,如下:

@font-face {font-family: "ytui-icon";
  src: url('./iconfont.eot?t=1531298415150'); /* IE9*/
  src: url('./iconfont.eot?t=1531298415150#iefix') format('embedded-opentype'), /* IE6-IE8 */
  url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAqUAAsAAAAAD4AAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAAQwAAAFZW7kguY21hcAAAAYAAAACRAAAB9vtUBxpnbHlmAAACFAAABigAAAhwKcmwImhlYWQAAAg8AAAALwAAADYToArbaGhlYQAACGwAAAAeAAAAJAmHBUJobXR4AAAIjAAAABkAAAAsLZ8AAGxvY2EAAAioAAAAGAAAABgKzAy8bWF4cAAACMAAAAAfAAAAIAEaAGBuYW1lAAAI4AAAAU8AAAJ5oPQT1nBvc3QAAAowAAAAYQAAAIRhYKFqeJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2BkUWOcwMDKwMHUyXSGgYGhH0IzvmYwYuRgYGBiYGVmwAoC0lxTGBwYKp6JMTf8b2CIYW5gaAAKM4LkAOEtC7EAeJzFkcsRwjAMRJ8TYyDDBMInVVACBXHi7BKoVG2EtZQLFUQzzxrJlu3ZBXZAL54iQ/qQaPFWN3m/Z/B+5qW6KCc6qs3Lorra6FlHtFc8d5rJurmw58DR5wubRdru6f84+fpdK6lCXdEXbQykHnYOmjN2CZo7NgXNNbsGTXO7BdIbuwdSHnsE8gCbA4YfemYcZwAAAHichZVdjBvVFcfvuXfmejxjj2c8M3fs9dieD3uGjZdd44+ZDZt1ApsSFdICjdVsUKISooh+kAKtSBBJ6PaLNhJFoJAH+kKKkBB0eaF55CEvKbT7wlOlqEJJP4RKhehTHqi0s71jJyg0rbCuzhzfjyP9zv/cc5GI0NZfyLukggx0G7oD7UYPIAS0A76K6+BFg3ncAcsTLdtUSRREXi7w58ky2D41WS8ehDbN0RKo0IC+14ujeRzBcDDCS9BjdYBqbWZfue2UyYsgV6LGz9N78WtgNQOnNLo9/ercTrPnGtKJQrlcLZefl6goShgLJRWO2Swv5mWavi6WZqx3m7O4CYVqNLP3QNGtlQ//cvD9etvOA6ytgVFz1Td26jM6H6dmmFGu5rSiVJkpBi0TTvxdqRiFevg3xH8Z63lykRxCVbSAltAetA+hts1M6i9wgsCbh0gfQeI1wNZNSvRBGHg+tXST9bM1vtQAwi33Vcg1AOZhEGfbTZoTMz+j5j55risbTO66D85tXo2GAMMIX+Rfp2GWNneVTLMEa3NdIC88tuPob8bHMD42ntj05YKuFzAqaFohRZm/lhnSlZkhd2sOj0PQNN7m+fn7oQ4Xs2CbqGT6R3c89gKBbvrkjWCZ3Q1aRYOJucqtrnMHITzJw1XyMGpnardZnMTJCPocm2NxuBusNqM5FSzu8DGdD7OdEzeYpCBb4XutKMym/Fx2AD9EKxU5zwI9+V2enPr697ZOPfMSFn9x1+BJAcqWWsBL22YfKXsnFu7YA5MJcAy7JUZHBTjzg+P/PDQ8gLWK175TUR1RaYcfQlGXhIJKTy584yTBZ589nT5y70+E/NuJGTpWhVrfmbvtzhGsLPSf9a1Ww6qUlwJ/Gzm8MP74+FNnMF7dTjSzWmkYomjpvA54CrZ+Kr5JDmYVIUIuDxEW0oU+Fsjr/TJ80N/8jDsIkUmePiT7UQ+N0H18rxUM+xbH5ayJMYImTCshiZltMH4NaJaFZYhRwpBtZbloN2AnxAkJoxxr88pgTWBZChP80IULX3m0dXvNwwGdrT2hfry+/onyw1qHtASv/k03litWHop/fA9KMszZb2y+TAx5vwgBiKcF/AeaOWPZIIaJd7nHHawXKQ8DqOZi7a0rgnB1XRPcxhbiAYvc2fXiA9X3ISeI6WfvVV/98crKa/kCLoIIiIL2/CtAt3gmiliRhydXsrsicPY1AZE1pCCb35SMPkTRlIxOHP6XohxDZDCifcYLwWIcOBuTqgkGI2FSV4MwCgNqBwZPHkaX0yuUgnf5MniUplcub4Aiium1jY30miiCokmFitOKIraa1JpQkhpcN0mjRBgLVNVd1ilRsaQ6i6dhfO3gwbWbI/HITR5h46aIeLc38JuOpeShV13phK7WqjJPZjNqUKnNug5rsTmmNDqB5sG5NDky1fwG93au+heooR8mPXty3ennnP3BiEvMFeUjjmLOGk7TYAwD61bgdO8YTi86akmkcpe5kiaLeIylXIH6rNqQSgBLyWqnHd6lUOl/EOJvnwNPa3UaCuvYLYV52myNtR3FlptVFmpu+GDch1Jhu68vpeqRb/2XjrNo7//XEWhW1oPYjjMtM0bD4l2RhkmYxLyI+ej3rrcEM9O7z5L4SwWFbZou5SBxmULzBdVR7348inkLEwgRSU47tC/i7cxa9mVJqqpuGH6ppM/xqlAV9z6AEjUVNdDb6e/7q6vw9J66XZR1qdj4tWcdtnbMOUVTkWSouVk936xrA+36YhYIi6PBtIFlylLOZmSYgc9Ld5BMtOW6+lNdLcqSXpzcQv6MOwQqUU1Rm6rDm65kLi6Hhq+rValclsKmxamjrx2xZIo5chw9fvct4r4Fe2uKScW8boRZwqT0UsIUVZa0RVvZVuNY3it+YBcr6p6nYXX/da5z5APyBCqjFhogZEyhrl/EcJm3KPGmJ4uj1Xk/ivk7laM+p4u5pL2uCPTSJaAifvVM5WffLdlnL+CHn/oVCB+tr38k4N+ekQvvLPJHKH/grwfy/GW75wKcz7aL6b8vnf3HUO54oqZ35/715+7Gj/gBfuylPwVYz9eHn8omk8djmZly1lf+AywRjK94nGNgZGBgAOIgnfsq8fw2Xxm4WRhA4Hr243wE/b+BdSVzA5DLwcAEEgUAK58LFAB4nGNgZGBgbvjfwBDDuo0BCFhXMjAyoAJuAGWQA9UAAHicY2FgYGB+ycDAwgDBrNsQbHQMADrxAcwAAAAAAAAAAHYA+gF6AY4CDAKIAvADdAPgBDh4nGNgZGBg4GYIYWBlAAEmIOYCQgaG/2A+AwASGgF7AHicbZE7TsNAFEWv80NxBEUQlDANFKA4nwYpJZGSgo4ifeKMnUS2xxqPI3kJrIc1sAJ6OtZAy40zuAjxaJ7Ove/jJw2ALr7g4PBd8R7YQZvqwDWc4cZynf6d5QbZs9xEB0+WW/SfLbt4xIvlDi6x4wSn0aZ6wJtlhzu8W67hAh+W6/Q/LTfI35abuMaP5Ra6zrllF3Pn1nIH907uTrRcGLkSy0JsfJUEKjFuYfJNb69eZZhHC13pCuZSZxuViKE3qLyZTKT+m5XtwpExgQi0isWUQ2UUKZFqtZW+8dbGpON+P7C+56uYq02gIbGAYVxBYImCcQMfCgmCMhrWFYw5/V6Ve2VHSC9itz6R/+/M2aGRUe+VwJDPNDhRN2NdUtYe75XxoUKM6BruJng1O2LS1G4quU9EFkjL3JaOT9/DuuxKMUafJziq98p/x7/PQHE2AHicbcZRCoAgEEXReZVZ1lZcVNiEUTiWCi2/IPrrwIVLFb0M/RtQoUYDhRYaHXoYDBgJ1xhPTs7uIluJKnoJrJxnt2nP05w4d0fhlFcJ5hu71HmN6sku2klIZc9ENxsEGrkAAAA=') format('woff'),
  url('./iconfont.ttf?t=1531298415150') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
  url('./iconfont.svg?t=1531298415150#ytui-icon') format('svg'); /* iOS 4.1- */
}
ERROR in ./~/css-loader!./~/postcss-loader!./~/sass-loader!./src/Icon/index.scss
    Module not found: Error: Can't resolve './iconfont.eot?t=1531298415150' in 'D:\Dev\yt\Nodejs\npm\YTUI\src\Icon'
     @ ./~/css-loader!./~/postcss-loader!./~/sass-loader!./src/Icon/index.scss 6:86-127 6:162-203
     @ ./src/Icon/index.scss
     @ ./src/Icon/index.js
     @ ./src/index.js

后来查阅资料时发现webpack不能识别css内的相对路径(js可以),将引用路径改成这样就可以:

@font-face {font-family: "ytui-icon";
  src: url('~/iconfont.eot?t=1531298415150'); /* IE9*/
  src: url('~/iconfont.eot?t=1531298415150#iefix') format('embedded-opentype'), /* IE6-IE8 */
  url('data:application/x-font-woff;charset=utf-8;base64,d09GRgABAAAAAAqUAAsAAAAAD4AAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABHU1VCAAABCAAAADMAAABCsP6z7U9TLzIAAAE8AAAAQwAAAFZW7kguY21hcAAAAYAAAACRAAAB9vtUBxpnbHlmAAACFAAABigAAAhwKcmwImhlYWQAAAg8AAAALwAAADYToArbaGhlYQAACGwAAAAeAAAAJAmHBUJobXR4AAAIjAAAABkAAAAsLZ8AAGxvY2EAAAioAAAAGAAAABgKzAy8bWF4cAAACMAAAAAfAAAAIAEaAGBuYW1lAAAI4AAAAU8AAAJ5oPQT1nBvc3QAAAowAAAAYQAAAIRhYKFqeJxjYGRgYOBikGPQYWB0cfMJYeBgYGGAAJAMY05meiJQDMoDyrGAaQ4gZoOIAgCKIwNPAHicY2BkUWOcwMDKwMHUyXSGgYGhH0IzvmYwYuRgYGBiYGVmwAoC0lxTGBwYKp6JMTf8b2CIYW5gaAAKM4LkAOEtC7EAeJzFkcsRwjAMRJ8TYyDDBMInVVACBXHi7BKoVG2EtZQLFUQzzxrJlu3ZBXZAL54iQ/qQaPFWN3m/Z/B+5qW6KCc6qs3Lorra6FlHtFc8d5rJurmw58DR5wubRdru6f84+fpdK6lCXdEXbQykHnYOmjN2CZo7NgXNNbsGTXO7BdIbuwdSHnsE8gCbA4YfemYcZwAAAHichZVdjBvVFcfvuXfmejxjj2c8M3fs9dieD3uGjZdd44+ZDZt1ApsSFdICjdVsUKISooh+kAKtSBBJ6PaLNhJFoJAH+kKKkBB0eaF55CEvKbT7wlOlqEJJP4RKhehTHqi0s71jJyg0rbCuzhzfjyP9zv/cc5GI0NZfyLukggx0G7oD7UYPIAS0A76K6+BFg3ncAcsTLdtUSRREXi7w58ky2D41WS8ehDbN0RKo0IC+14ujeRzBcDDCS9BjdYBqbWZfue2UyYsgV6LGz9N78WtgNQOnNLo9/ercTrPnGtKJQrlcLZefl6goShgLJRWO2Swv5mWavi6WZqx3m7O4CYVqNLP3QNGtlQ//cvD9etvOA6ytgVFz1Td26jM6H6dmmFGu5rSiVJkpBi0TTvxdqRiFevg3xH8Z63lykRxCVbSAltAetA+hts1M6i9wgsCbh0gfQeI1wNZNSvRBGHg+tXST9bM1vtQAwi33Vcg1AOZhEGfbTZoTMz+j5j55risbTO66D85tXo2GAMMIX+Rfp2GWNneVTLMEa3NdIC88tuPob8bHMD42ntj05YKuFzAqaFohRZm/lhnSlZkhd2sOj0PQNN7m+fn7oQ4Xs2CbqGT6R3c89gKBbvrkjWCZ3Q1aRYOJucqtrnMHITzJw1XyMGpnardZnMTJCPocm2NxuBusNqM5FSzu8DGdD7OdEzeYpCBb4XutKMym/Fx2AD9EKxU5zwI9+V2enPr697ZOPfMSFn9x1+BJAcqWWsBL22YfKXsnFu7YA5MJcAy7JUZHBTjzg+P/PDQ8gLWK175TUR1RaYcfQlGXhIJKTy584yTBZ589nT5y70+E/NuJGTpWhVrfmbvtzhGsLPSf9a1Ww6qUlwJ/Gzm8MP74+FNnMF7dTjSzWmkYomjpvA54CrZ+Kr5JDmYVIUIuDxEW0oU+Fsjr/TJ80N/8jDsIkUmePiT7UQ+N0H18rxUM+xbH5ayJMYImTCshiZltMH4NaJaFZYhRwpBtZbloN2AnxAkJoxxr88pgTWBZChP80IULX3m0dXvNwwGdrT2hfry+/onyw1qHtASv/k03litWHop/fA9KMszZb2y+TAx5vwgBiKcF/AeaOWPZIIaJd7nHHawXKQ8DqOZi7a0rgnB1XRPcxhbiAYvc2fXiA9X3ISeI6WfvVV/98crKa/kCLoIIiIL2/CtAt3gmiliRhydXsrsicPY1AZE1pCCb35SMPkTRlIxOHP6XohxDZDCifcYLwWIcOBuTqgkGI2FSV4MwCgNqBwZPHkaX0yuUgnf5MniUplcub4Aiium1jY30miiCokmFitOKIraa1JpQkhpcN0mjRBgLVNVd1ilRsaQ6i6dhfO3gwbWbI/HITR5h46aIeLc38JuOpeShV13phK7WqjJPZjNqUKnNug5rsTmmNDqB5sG5NDky1fwG93au+heooR8mPXty3ennnP3BiEvMFeUjjmLOGk7TYAwD61bgdO8YTi86akmkcpe5kiaLeIylXIH6rNqQSgBLyWqnHd6lUOl/EOJvnwNPa3UaCuvYLYV52myNtR3FlptVFmpu+GDch1Jhu68vpeqRb/2XjrNo7//XEWhW1oPYjjMtM0bD4l2RhkmYxLyI+ej3rrcEM9O7z5L4SwWFbZou5SBxmULzBdVR7348inkLEwgRSU47tC/i7cxa9mVJqqpuGH6ppM/xqlAV9z6AEjUVNdDb6e/7q6vw9J66XZR1qdj4tWcdtnbMOUVTkWSouVk936xrA+36YhYIi6PBtIFlylLOZmSYgc9Ld5BMtOW6+lNdLcqSXpzcQv6MOwQqUU1Rm6rDm65kLi6Hhq+rValclsKmxamjrx2xZIo5chw9fvct4r4Fe2uKScW8boRZwqT0UsIUVZa0RVvZVuNY3it+YBcr6p6nYXX/da5z5APyBCqjFhogZEyhrl/EcJm3KPGmJ4uj1Xk/ivk7laM+p4u5pL2uCPTSJaAifvVM5WffLdlnL+CHn/oVCB+tr38k4N+ekQvvLPJHKH/grwfy/GW75wKcz7aL6b8vnf3HUO54oqZ35/715+7Gj/gBfuylPwVYz9eHn8omk8djmZly1lf+AywRjK94nGNgZGBgAOIgnfsq8fw2Xxm4WRhA4Hr243wE/b+BdSVzA5DLwcAEEgUAK58LFAB4nGNgZGBgbvjfwBDDuo0BCFhXMjAyoAJuAGWQA9UAAHicY2FgYGB+ycDAwgDBrNsQbHQMADrxAcwAAAAAAAAAAHYA+gF6AY4CDAKIAvADdAPgBDh4nGNgZGBg4GYIYWBlAAEmIOYCQgaG/2A+AwASGgF7AHicbZE7TsNAFEWv80NxBEUQlDANFKA4nwYpJZGSgo4ifeKMnUS2xxqPI3kJrIc1sAJ6OtZAy40zuAjxaJ7Ove/jJw2ALr7g4PBd8R7YQZvqwDWc4cZynf6d5QbZs9xEB0+WW/SfLbt4xIvlDi6x4wSn0aZ6wJtlhzu8W67hAh+W6/Q/LTfI35abuMaP5Ra6zrllF3Pn1nIH907uTrRcGLkSy0JsfJUEKjFuYfJNb69eZZhHC13pCuZSZxuViKE3qLyZTKT+m5XtwpExgQi0isWUQ2UUKZFqtZW+8dbGpON+P7C+56uYq02gIbGAYVxBYImCcQMfCgmCMhrWFYw5/V6Ve2VHSC9itz6R/+/M2aGRUe+VwJDPNDhRN2NdUtYe75XxoUKM6BruJng1O2LS1G4quU9EFkjL3JaOT9/DuuxKMUafJziq98p/x7/PQHE2AHicbcZRCoAgEEXReZVZ1lZcVNiEUTiWCi2/IPrrwIVLFb0M/RtQoUYDhRYaHXoYDBgJ1xhPTs7uIluJKnoJrJxnt2nP05w4d0fhlFcJ5hu71HmN6sku2klIZc9ENxsEGrkAAAA=') format('woff'),
  url('~/iconfont.ttf?t=1531298415150') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
  url('~/iconfont.svg?t=1531298415150#ytui-icon') format('svg'); /* iOS 4.1- */
}

就打包成功了,webpack: Compiled successfully.


参考资料:

Object.assign vs $.extend ?

在业务工作中,如果你需要封装一个组件,你肯定会遇到这样的场景:组件的默认的配置对象defaultOption,需要与暴露出去的api进行合并,保证用户自定义的值能覆盖默认配置,但未定义的值取默认配置。其实,这本质上就是需要把defaultOption对象与外部传入的配置对象进行合并。我们经常会看到两种写法:Object.assign$.extend,那这两者有没有区别呢?

Object.assign()和$.extend()用法

首先呈现官方文档:Object.assign()jQuery.extend()

  • Object.assign()
    Object.assign(target, ...sources),Returns: Object

  • $.extend()
    jQuery.extend( target [, object1 ] [, objectN ] ), Returns: Object
    jQuery.extend( [deep ], target, object1 [, objectN ] ), If true, the merge becomes recursive(递归) (aka. deep copy,深拷贝).

The Object.assign() method only copies enumerable and own properties from a source object to a target object.

$.extends(): Merge the contents of two or more objects together into the first object.

区别

两者区别其实不大:

  • 主要在于$.extend()如果给第一个参数传入true可实现deep merge,而Object.assign只会copies that reference value
  • 另外,就是使用$.extend()需要提前引入jQuery

Talk is Cheap, show me the code

  • 如果不想改变target object,可以将第一个参数传入空对象{}
var opts = Object.assign( {}, defaults, options );
// 或者
var opts = $.extend( {}, defaults, options );
  • Polyfill of Object.extend()
if (typeof Object.assign != 'function') {
  // Must be writable: true, enumerable: false, configurable: true
  Object.defineProperty(Object, "assign", {
    value: function assign(target, varArgs) { // .length of function is 2
      'use strict';
      if (target == null) { // TypeError if undefined or null
        throw new TypeError('Cannot convert undefined or null to object');
      }

      var to = Object(target);

      for (var index = 1; index < arguments.length; index++) {
        var nextSource = arguments[index];

        if (nextSource != null) { // Skip over if undefined or null
          for (var nextKey in nextSource) {
            // Avoid bugs when hasOwnProperty is shadowed
            if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
              to[nextKey] = nextSource[nextKey];
            }
          }
        }
      }
      return to;
    },
    writable: true,
    configurable: true
  });
}
  • 如何使用Object.assign()实现deep merge
function isObject(item) {
  return (item && typeof item === 'object' && !Array.isArray(item));
}

function mergeDeep(target, ...sources) {
  if (!sources.length) return target;
  const source = sources.shift();

  if (isObject(target) && isObject(source)) {
    for (const key in source) {
      if (isObject(source[key])) {
        if (!target[key]) Object.assign(target, { [key]: {} });
        mergeDeep(target[key], source[key]);
      } else {
        Object.assign(target, { [key]: source[key] });
      }
    }
  }
  return mergeDeep(target, ...sources);
}

参考资料

(本篇完)

从js原型继承深入理解instanceof操作符

在学习js原型继承时,有如下代码:

function SuperType(){
	this.property = true;
}

SuperType.prototype.getProperty = function(){
	return this.property;
}


function SubType(){
	this.subProperty = false;
}

SubType.prototype = new SuperType();

SubType.prototype.getSubProperty = function(){
	return this.subProperty;
}

var instance = new SubType();

然后,再执行instance instanceof SubType时,得到的结果的true。

最开始了解instanceof操作符时,会认为这个操作符的含义是是否是XX的实例的含义。

但在理解js的原型继承时,构造函数(引用类型)的原型对象被重写为另外一个引用类型(构造函数)的实例,这里新的原型对象并没有包含指向构造函数的constructor属性,所以我就会认为“新创建的实例并不是原引用类型(构造函数)的实例”,认为上面应该返回false。

这样的理解其实是片面的,虽然在代码原理层面,原型链和原构造函数已经没有了任何关系(因为重写的原型对象没有构造函数属性了)。但是,继承的含义却保证了“实例是原引用类型(构造函数)的实例”。

再从instanceof操作符的原理上看,深入可参见这篇文章JavaScript instanceof 运算符深入剖析,可以了解到其本质含义是:“沿着原型链来查找是否包含指定构造函数(引用类型)的原型对象”,而构造函数到原型对象的单向引用还是保持的。

综上,instance instanceof SubType返回true就不难理解了。

iPhone X H5 适配通用方案

说明

iPhone X 适配理论上需要在 UI 预先适配的基础上再做前端页面的适配,以保证 iPhone X 下的最佳体验。但因为项目复杂,涉及改动点较多,可暂时采用此方案。

局限性主要体现在fixed元素无法使用通用的样式适配。且涉及到是否吸底,吸底元素是否可以简单移动位置还是需要增加元素高度又能保证中间内容看起来“垂直居中”。

需要适配点

  • 整体页面显示位置(内容)
  • fixed + bottom = 0
  • fixed + bottom > 0
  • absolute + bottom = 0
  • 瀑布流列表加载中文案可能触底 - 组件内修改
  • 沉浸(状态)式H5页面头部高度 - 需个性化修改

通用解决方案

针对前面4个适配点,给出适配方案如下:

1.修改 meta 标签中,加入viewport-fit=cover,使在 iPhone X 下页面全屏覆盖,并且后面相应的 hack 属性能生效。

<meta name="viewport" content="...,viewport-fit=cover">

2.修改 body 内边距。在通用 CSS 中添加下面代码。其中constantenv是 iOS 10+ 上才能识别的 CSS 方法,safe-area-inset-bottom是底部的安全距离,一般是34px(防止 iPhone X Plus 之类的,还是用个统一的官方变量比较好)(注意:样式需要放到最后,保证不会被覆盖)

@supports (padding-bottom: constant(safe-area-inset-bottom)) {
    body {
        padding-bottom: constant(safe-area-inset-bottom);
        padding-bottom: env(safe-area-inset-bottom);
    }
}

3.针对fixed定位的元素,因为其是相对于浏览器窗口进行定位的,所以 body 的内容区域无法限制其位置,需要调整其位置。采用以下通用 js 解决。(注意:需放置页面最后,保证页面元素已渲染到页面,用户能感知到页面中 fixed 定位元素会在显示后往上移一段,至于是否会遮挡到中间滚动块还待观察,因此建议在项目允许的情况下,对fixed元素最好用 CSS 做个性化适配)

// 底部安全距离
const IPHONEX_BOTTOM_HEIGHT = 34;

// 可添加到通用UA判断中
function isIphoneX(){
    return /iphone/gi.test(window.navigator.userAgent) && (screen.height == 812 && screen.width == 375)
}

if(isIphoneX()){
    $('*').filter(function(index, item){
        return $(item).css('position') === 'fixed';
    }).map(function(index, item){
        var itemTop = $(item).position().top;
        if(itemTop > IPHONEX_BOTTOM_HEIGHT){
            $(item).css('top', itemTop - IPHONEX_BOTTOM_HEIGHT);
        }
    });
}

参考

前端实现瀑布流列表考虑点总结

随着移动设备性能的增强,“瀑布流H5”是移动端当前比较流行的一种产品形态,但是在实现过程中,特总结需要注意几点内容:

  1. 分页
  • 分页方式
    • 根据page_size+page_num
    • 根据id+page_num
  1. 列表状态
  • 正常滚动状态
  • 编辑状态
  1. 动作
  • 滚动
    • 滚动区域
      • 局部滚动
      • 全局滚动
      • 局部、全局相结合(参考支付宝首页)
    • 滚动动作/事件(回调)
      • 上拉
      • 下拉
      • 左滑
      • 右滑
  1. 更新
  • append/prepend
  • 排序
  • 内容修改
  • 置顶/置底
  1. 效果/动画
  • 懒加载
  • 删除
  • 插入
  • loading

display:flex与word-space:no-wrap的冲突

近期切图时,遇到一个问题,在使用flex布局时,容器内子元素获得的宽度,当其设置word-space:no-wrap会失效。

在网上查了查资料,说是会打乱flex布局:具体原因是设置了word-space:no-wrap后元素就没办法伸缩了。

解决办法是:
将子元素的width设置为0

设置0后就有了固定尺寸就可以收缩了
关于display:flex碰上white-space nowrap的问题

《JavaScript高级程序设计(第3版)》学习笔记——一、JavaScript简介

Code:http://www.wrox.com/WileyCDA/WroxTitle/Professional-JavaScript-for-Web-Developers-3rd-Edition.productCd-1118026691,descCd-DOWNLOAD.html

简介

JavaScript (web)主要包含以下内容

  • ECMAScript - 核心
    宿主环境:不仅提供基本的ECMAScript实现,同时也会提供该语言的扩展(如DOM、BOM等),以便语言与环境之间的对接交互。

  • DOM - 文档对象模型
    文档对象模型(DOM,Document Object Model)是针对XML但经过扩展用于HTML的应用程序编程接口(API,Application Programming Interface)。

DOM级别与W3C标准。

  • BOM - 浏览器对象模型
    浏览器对象模型(BOM,Browser Object Model)。

BOM标准?

  • How use JavaScript in HTML
    借助**<script>元素**, 使用<script>元素的方式有两种:
  • 直接在页面嵌入JavaScript代码
    • 只须为<script>指定type属性text/javascript,然后把js代码直接放入元素内即可
    • 从上到下依次解释(解释器)
    • 在完成之前,页面中的其余内容都不会被浏览器加载或显示(阻塞)
  • 包含外部JavaScript文件
    • 指定src属性为外部js文件链接
    • 会被加载到当前页面中(然后就和方式一相同)
    • 指定src属性会忽略标签包含的代码
    • 可以指定来自外部域的js文件

执行顺序:

  • 除非指定了defer属性async属性,否则浏览器会按照**<script>元素在页面中出现的顺序**对它们依次解析
  • defer:延迟脚本。表示脚本可以延迟到文档完全被解析和显示之后再执行(Q:下载是否会阻塞页面?)。立即下载,延迟顺序执行(理论上,但实际可能并不会按照顺序)
  • async:异步脚本。表示立即下载脚本,但不妨碍页面中的其他操作(N:异步体现在同时页面可进行其他操作)。立即下载,延迟无序执行
  • (延迟脚本和异步脚本,下载、执行与load事件、DOMContentLoaded事件的关系?)

<script>标签的位置:一般应当放在<body>内容的后面(用户体验)

尽量使用外部文件引入的方式,而不要嵌入代码的原因:

  • 可维护
  • 可缓存:两个页面都是用同一个文件,就只需下载一次
  • 适应未来:嵌入代码语法在传统HTML内需要兼容,而引入外部文件的语法是都支持的

《JavaScript高级程序设计(第3版)》学习笔记——九、DOM

DOM

The Document Object Model,文档对象模型。描绘了一个层次化的节点树。

节点层次

DOM 可以将任何 HTML 或 XML 文档描绘成一个由多层节点构成的结构。

节点分为几种不同的类型,表示文档中不同的信息/标记。每个节点都拥有各自的特点、数据和方法。节点与其他节点存在某种关系,构成了层次。而所有页面标记则表现为一个以特定节点为根节点的树形结构。

NODE 类型

基类型

每个节点都有一个 nodeType 属性,用于表明节点的类型。

  • Node.ELEMENT_NODE(1)
  • Node.ATTRIBUTE_NODE(2)
  • Node.TEXT_NODE(3)
  • Node.CDATA_SECTION_NODE(4)
  • Node.ENTITY_REFERENCE_NODE(5)
  • Node.ENTITY_NODE(6)
  • Node.PROCESSING_INSTRUCTION_NODE(7)
  • Node.COMMENT_NODE(8)
  • Node.DOCUMENT_NODE(9)
  • Node.DOCUMENT_TYPE_NODE(10)
  • Node.DOCUMENT_FRAGMENT_NODE(11)
  • Node.NOTATION_NODE(12)
nodeName 和 nodeValue 属性
节点关系

childNodes 属性,其中保存着一个 NodeList 对象:

  • 类数组对象,用于保存一组有序的节点
  • length
  • 基于 DOM 结构动态执行的结果,因此 DOM 结构的变化能够自动反映在 NodeList 对象上
  • 索引(方括号)或使用 item() 方法访问
  • Array.prototype.slice.call()

parentNode 属性,指向文档树中的父节点

previousSibling 和 nextSibling 属性,可以访问同一列表的其他节点,没有值为 null

firstChild 和 lastChild 属性。父节点的属性,分别指向其 childNodes 列表中的第一个和最后一个节点,即:

someNode.firstNode === someNode.childNodes[0];  // true
someNode.lastNode === someNode.childNodes[someNode.childNodes.length - 1];    // true

hasChildNodes()

ownerDocument 属性:指向表示整个文档的文档节点

操作节点

因为关系节点都是只读的,所以 DOM 提供了一些操作节点的方法。

appendChild(),用于向 childNodes 列表的末尾添加一个节点,返回新增的节点。如果已经是文档的一部分了,结果就是将该节点从原来的位置转移到新位置。

insertBefore(),将节点放在 childNodes 列表中某个特定的位置上,接受两个参数:要插入的节点和作为参照的节点,插入节点会变成参照节点的前一个同胞节点(previousSibling),同时被返回。如果参照节点是 null,则与 appendChild() 执行相同的操作。

replaceChild(),接受两个参数:要插入的节点和要替换的节点。插入节点的所有关系指针都会从被替换节点复制过来,技术上,被替换节点仍然还在文档中,但在文档中已经没有了位置。

removeChild(),要移除的节点,返回被移除的节点。同样还在文档中,但没有位置了

注意:上面四个方法操作的都是某个节点的子节点,使用必须先取得父节点。如果在不支持子节点的节点上调用了这些方法,将会导致错误发生。

其他方法

cloneNode(),用于创建调用这个方法的节点的一个完全相同的副本,接受一个布尔值参数,表示是否执行深复制(是否复制子节点树)。复制后返回的节点副本属于文档所有,但并没有为它指定父节点。注意:cloneNode() 不会复制添加到 DOM 节点中的 JavaScript 属性

normalize(),唯一作用处理文档树中的文本节点。

Document 类型

Document 类型表示文档。

在浏览器中,document 对象是 HTMLDocument (继承自 Document) 类型的一个实例,表示整个 HTML 页面。

documetElement 属性:始终指向 HTML 页面中的 <html> 元素。

document.documentElement === document.childNodes[0];    // true

body 属性:直接指向 HTML 页面中的 <body> 元素。

文档信息:

  • document.title:可读写,修改当前页面的标题并反映在浏览器的标题栏上。
  • document.URL
  • document.domain
  • document.referrer

查找元素(返回结果都是“动态”集合):

  • document.getElementById()
  • document.getElementsByTagName():返回一个“动态”集合
// 取得文档中的所有元素
document.getElementsByTagName("*");
  • document.getElementsByName()

特殊集合:

  • document.anchors,包含文档中所有带 name 特性的 a 元素
  • document.forms, === document.getElementsByTagName('form');
  • document.images, === document.getElementsByTagName('img')
  • document.links,包含文档中所有带 href 特性的 a 元素

文档写入:输入流写入到网页中。

  • document.write()
  • document.writeln()
  • document.open()
  • document.close()
<!-- 在页面呈现过程中直接向其中输出内容 -->
<script type="text/javascript">
    // 利用write/writeln动态包含外部资源
    document.write("<script type=\"text/javascript\" src=\"file.js\">" + "<\/script>");
</script>
<!-- 页面加载完全之后执行write会重写整个页面内容 -->

Element 类型

所有 HTML 元素都由 HTMLElement 类型表示(或子类型),HTMLElement 类型直接继承于 Element 类型,并添加了一些属性:id、title、lang、dir、className等(可通过对象属性访问和设置特性值)。

nodeName属性、tagName属性(注意大小写)

操作特性:

  • element.getAttribute():除默认特性外,也可以取得自定义特性。特性名称不区分大小写。自定义特性应该加上data-前缀以便验证
  • element.setAttribute():设置特性名会被统一转换为小写形式
  • element.removeAttribute():不仅清除特性的值,也会从元素中完全删除特性

对象属性名 vs ...Attribute()

  • style 属性
    .style返回一个对象,而 getAttribute() 会返回CSS文本
  • onclick 等事件处理程序
    .onclick 返回 JavaScript 代码(函数,没有为null),而 getAttribute() 会返回相应代码的字符串

element.attributes 属性,包含一个 NamedNodeMap,也是一个“动态”集合。

创建元素:document.createElement()

元素的子节点:element.getElementsByTagName(),搜索起点是当前元素

Text 类型

nodeValue 和 data 属性

  • appendData(text)
  • deleteData(offset, count)
  • insertData(offset, text)
  • replaceData(offset, count, text)
  • splitText(offset)
  • substringData(offset, count)

length 属性

在向 DOM 文档中插入文本之前,先对其进行 HTML 编码的一种有效方式。(修改文本节点时,会经过 HTML 编码)

创建文本节点:document.createTextNode(),作为参数的文本也将按照 HTML/XML 格式进行编码

规范化文本节点:element.normalize()(将相邻文本节点合并,在父元素上调用)

分隔文本节点:splitText()

Comment 类型

CDATASection 类型

DocumentType 类型

DcoumentFragment 类型

在所有节点类型中,只有 DocumentFragment 在文档中没有对应的标记。“轻量级”的文档,可以包含和控制节点,但不会像完整的文档那样占用额外的资源。

document.createDocumentFragment()

文档片段继承了 Node 的所有方法,通常用于执行那些针对文档的 DOM 操作。如果将文档中的节点添加到文档片段中,就会从文档树中移除该节点,也不会从浏览器中再看到该节点,添加到文档片段中的新节点同样也不属于文档树。可以通过 appendChild/insertBefore 将文档片段中内容添加到文档中。在将文档片段作为参数传递给这两个方法时,实际只会将文档片段的所有子节点添加到相应位置,文档片段本身永远不会成为文档树的一部分。(可以避免多次直接操作DOM)

var fragment = document.createDocumentFragment();
var ul = document.getElementById("myList");
var li = null;
for (var i=0; i < 3; i++){
    li = document.createElement("li");
    li.appendChild(document.createTextNode("Item " + (i+1)));
    fragment.appendChild(li);
}
ul.appendChild(fragment);

Attr 类型

元素的特性在 DOM 中以 Attr 类型表示。

不建议直接访问特性节点,实际上使用getAttribute()、element.setAttribute()、element.removeAttribute()方法更加方便。

DOM 操作

动态脚本

页面加载时不存在,但在将来的某一时刻通过修改 DOM 动态添加的脚本。创建动态脚本的两种方式:插入外部文件和直接插入 JavaScript 代码。

动态加载的外部 JavaScript 文件能够立即运行。

<script type="text/javascript" src="client.js"></script>

创建这个节点的 DOM 代码:

var script = document.createElement('script');
script.type = 'text/javascript';
script.src = 'client.js';
document.body.appendChild(script);  // 在执行最后一行代码把<script>元素添加到页面中之前,是不会下载外部文件的。

但是浏览器目前没有标准方式知道脚本加载完成。

动态直接插入 js 代码:

var script = document.createElement('script');
script.type = "text/javascript";
script.appendChild(document.createTextNode("function sayHi(){alert('hi');}"));
// script.text = "function sayHi(){alert('hi');}"; // IE 中
document.body.appendChild(script);

动态样式

var link = document.createElement("link");
link.rel = "stylesheet";
link.type = "text/css";
link.href = "style.css";
var head = document.getElementsByTagName("head")[0];
head.appendChild(link);

加载外部样式的过程是异步的。

操作表格

使用 NodeList

NodeList、NamedNodeMap、HTMLCollection,都是“动态”集合

小结

理解 DOM 的关键,就是理解 DOM 对性能的影响。DOM 操作往往是 JavaScript 程序中开销最大的部分,而因访问 NodeList 导致的问题最多,因为 NodeList 对象都是“动态”的,每次访问都会运行一次查询。=>尽量减少 DOM 操作。

配色收集

方案一

HEX模式和RGB模式
color0: #5FD9CD__RGB(95,217,205)
color1: #EAF786__RGB(234,247,134)
color2: #FFB5A1__RGB(255,181,161)
color3: #B8FFB8__RGB(184,255,184)
color4: #B8F4FF__RGB(184,244,255)
default

方案二

HEX模式和RGB模式
color0: #B8FFEA__RGB(184,255,234)
color1: #FFEBEF__RGB(255,235,239)
color2: #C9FFFC__RGB(201,255,252)
color3: #FFFCE6__RGB(255,252,230)
color4: #F6EBFC__RGB(246,235,252)
default


Source:http://www.peise.net/

支付宝小程序踩坑记

  • this.setData是同步的,无回调方法(与React中setState不同)
  • a:if不保存input状态(页面元素),会重新根据data初始化
  • 使用scroll-view横向滚动时,应在容器设置white-space: nowrap;(否则会换行)
  • 部分效果在真机上有效,在IDE中无效
    • view 的 disableScroll
    • input 的 maxLength
  • 在data中不可直接设置为my.getStorage...的值,取不到值
  • 支付宝小程序选择城市组件获取直辖市cityCode是省级的cityCode
  • 小程序内返回后,返回前页面还可能不被销毁(保留了状态),得到官方确认是5min才会销毁,实际表现可能不到5min

《JavaScript高级程序设计(第3版)》学习笔记——十、事件

事件

JavaScript 与 HTML 之间的交互是通过事件实现的。事件就是文档或浏览器窗口中发生的一些特定的交互瞬间。可以使用 侦听器(或处理事件) 来预订事件,以便事件发生时执行相应的代码。——“观察者模式”

事件流

事件流:描述的是从页面中接收事件的顺序。

  • 事件冒泡(event bubbling):即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。
  • 事件捕获(event capturing):不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。

DOM事件流(规范):“DOM2级事件”规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会,然后是具体的目标接收到事件,最后一个阶段是冒泡阶段,可以在这个阶段对事件作出响应。

事件处理程序

事件就是用户或浏览器自身执行某种动作,而响应某个事件的函数就叫做事件处理程序(或事件侦听器),为事件指定处理程序的方式有好几种:

  • HTML 事件处理程序:某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的 HTML 特性来指定,特性值是能够执行的 JavaScript 代码。

可以包含要执行的具体动作,也可以调用在页面其他地方定义的脚本。事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码。通过局部变量 event,可以直接访问事件对象,无需定义,也不用从函数的参数列表中读取。函数内部,this 值等于事件的目标元素

缺点:
- 存在时差问题,HTML 元素一出现就可能会触发事件,但事件处理程序有可能尚不具备执行条件,需要做好容错处理(try...catch)
- HTML 与 JavaScript 代码紧密耦合
- http://www.jibbering.com/faq/names/event_handler.html

  • DOM 0 级事件处理程序:通过 js 指定事件处理程序的传统方式,就是将一个函数赋值给一个函数处理程序属性,但前提是必须取得操作对象的引用

每个元素(包括 window 和 document)都有自己的事件处理程序属性,这些属性通常全部小写,将这些属性的值设置为一个函数,就可以指定事件处理程序。使用 DOM 0 级方法指定的事件处理程序被认为是元素的方法,因此,这时候的事件处理程序是在元素的作用域中运行,换句话说,程序中的 this 引用当前元素。

var btn = document.getElementById("myBtn");
btn.onclick = function(){
    alert(this.id); //"myBtn"
};

以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理

删除通过 DOM 0 级方法指定的事件处理程序的方法,将事件处理程序属性的值设为 null 即可:

btn.onclick = null;

(如果使用 HTML 指定事件处理程序,那么在 js 中元素事件处理程序属性的值就是一个包含着在同名 HTML 属性中指定的代码的函数。将属性值置为 null,也可以删除使用 HTML 指定的事件处理程序)

  • DOM 2 级事件处理程序

定义了两个方法,用于处理指定和删除事件处理程序:addEventListener()removeEventListener(),所有 DOM 节点中都包含这两个方法,并且都接受 3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。布尔值为true:表示在捕获阶段调用事件处理程序;布尔值为false:表示在冒泡阶段调用事件处理程序。

事件处理程序也在在其依附的元素的作用域中运行的。使用 DOM 2 级方法可以添加多个事件处理程序(按照添加顺序触发)。

通过 addEventListener() 添加的事件处理程序只能使用 removeEventListener() 来移除,移除时传入的参数与添加事件处理程序时使用的参数相同=>匿名函数无法移除

大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器(why???,现代浏览器应当都能处理了吧)。最好只在需要 在事件到达目标之前截获它的时候 将事件处理程序添加到捕获阶段,如果不是特别需要,不建议在事件捕获阶段注册事件处理程序。(why???)

  • IE 事件处理程序与跨浏览器的事件处理程序(jQuery源码)
    attachEvent()detachEvent()

https://github.com/jquery/jquery/blob/899c56f6ada26821e8af12d9f35fa039100e838e/src/event.js#L196

事件对象

在触发 DOM 上的某个事件时,会产生一个事件对象 event,包含着所有与事件有关的信息。

DOM 中的事件对象。兼容 DOM 的浏览器会将一个 event 对象传入到事件处理程序中,无论指定事件处理程序时使用什么方法(DOM 0 级/DOM 2 级),都会传入 event 对象。在通过 HTML 特性指定事件处理程序时,变量 event 中保存着 event 对象。

var btn = document.getElementById("myBtn");
    btn.onclick = function(event){
    alert(event.type); //"click"
};
btn.addEventListener("click", function(event){
    alert(event.type); //"click"
}, false);
<input type="button" value="Click Me" onclick="alert(event.type)"/>

event 对象包含与创建它的特定事件有关的属性和方法,触发的事件类型不一样,可用的属性和方法也不一样。

  • bubbles:Boolean,只读,表明事件是否冒泡
  • cancelable:Boolean,只读,表明是否可以取消事件的默认行为
  • currentTarget:Element,只读,其事件处理程序当前正在处理事件的那个元素。
  • defaultPrevented:Boolean,只读,为 true 表示已经调用了 preventDefault()。(DOM 3 级事件中新增)
  • detail:Integer,只读,与事件相关的细节信息
  • eventPhase:Integer,只读,调用事件处理程序的阶段:1-捕获阶段,2-“处于目标”阶段,3-冒泡阶段
  • preventDefault():Function,只读,取消事件的默认行为,前提是cancelable 属性为 true,才可以使用这个方法
  • stopImmediatePropagation():Function,只读,取消事件的进一步捕获或冒泡,同时阻止其他任何事件处理程序被调用。(DOM 3 级事件中新增)
  • stopPropagation():Function,只读,取消事件的进一步捕获或冒泡,前提是**bubbles 属性为 true **,才可以使用这个方法
  • target:Element,只读,事件的目标
  • trusted:Boolean,只读,为 true 表示事件是浏览器生成的,为 false 表示事件是由开发人员通过 JavaScript 创建的。(DOM 3 级事件中新增)
  • type:String,只读,被触发的事件的类型
  • view:AbstractView,只读,与事件关联的抽象视图,等同于发生事件的 window 对象

在事件处理程序内部,对象 this 始终等于 currentTarget 的值,而 target 则只包含事件的实际目标。如果直接将事件处理程序指定给了目标元素,则 this、currentTarget、target包含相同的值。

var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    alert(event.currentTarget === this); //true
    alert(event.target === this); //true
};

document.body.onclick = function(event){
    alert(event.currentTarget === document.body); //true
    alert(this === document.body); //true
    alert(event.target === document.getElementById("myBtn")); //true
};

在需要通过一个函数处理多个事件时,可以使用 type 属性:

var btn = document.getElementById("myBtn");
var handler = function(event){
    switch(event.type){
        case "click":
            alert("Clicked");
            break;
        case "mouseover":
            event.target.style.backgroundColor = "red";
            break;
        case "mouseout":
            event.target.style.backgroundColor = "";
            break;
    }
};
btn.onclick = handler;
btn.onmouseover = handler;
btn. onmouseout = handler;

要阻止特定事件的默认行为,可以使用 preventDefault() 方法。例如链接的默认行为就是在被单击时会导航到其 href 特性指定的 URL。如果你想组织链接导航这一默认行为,那么通过链接的 onclick 事件处理程序可以取消。只有 cancelable 属性设置为 true 的事件,才可以使用 preventDefault() 来取消其默认行为。

var link = document.getElementById("myLink");
link.onclick = function(event){
    event.preventDefault();
};

stopPropagation() 方法用于立即阻止事件在 DOM 层次中的传播,即取消进一步的事件捕获或冒泡。

eventPhase 属性,可以用来确定事件当前正位于事件流的哪个阶段。

  • 如果是在捕获阶段调用的事件处理程序,那么 eventPhase 属性值为 1
  • 如果事件处理程序处于目标对象上,则 eventPhase 属性值为 2
  • 如果是在冒泡阶段调用的事件处理程序,eventPhase 属性值为 3
    需要注意的是,尽管“处于目标”发生在冒泡阶段,但 eventPhase 仍然等于 2
var btn = document.getElementById("myBtn");
btn.onclick = function(event){
    alert(event.eventPhase); //2
};
document.body.addEventListener("click", function(event){
    alert(event.eventPhase); //1
}, true);
document.body.onclick = function(event){
    alert(event.eventPhase); //3
};

梳理下相关概念:

  • 首先事件流描述的是从页面中接收事件的顺序。DOM事件流总是不变的,某个事件被触发后,一般都是会经历捕获阶段处于目标阶段冒泡阶段,以click事件为例,当触发时,会经历document - html - body - ... - 具体元素 - ... - body - html - document,可以理解为在事件流的“流动”过程中,每经过一个节点都可以在上面做事情(类似于钩子函数的概念),而这个就是事件处理函数,而在这个过程中,每个节点都可以注册一个函数(当事件流到达节点时,就会触发函数的执行)。
  • 注册事件处理函数的节点称之为 currentTarget (事件处理程序中的 this 也指向这个节点),而在事件开始触发的那个节点是 target。以上述点击事件为例,在那一个点上最开始接触到实时点击的就是 target
  • 事件流经过的地方如果注册了相应的事件处理程序,就会被触发

只有在事件处理程序执行期间,event 对象才会存在;一旦事件处理程序执行完成,event 对象就会被销毁

IE 中的事件对象与跨浏览器的事件对象。(暂不深入研究)

常用正则收集

function isValidIdCardNo(cardNo) {
var reg = /(^[1-9]\d{5}(18|19|([23]\d))\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$)|(^[1-9]\d{5}\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\d{2}$)/;
    return reg.test(cardNo);
}

Install Node in MAC

通过 mac 自带的 ruby 套件一键安装 homebrew

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

homebrew 官网:https://brew.sh/

// 安装 Node
brew install node

《JavaScript高级程序设计(第3版)》学习笔记——十六、Ajax

Ajax

Ajax: A New Approach to Web Applications

Asynchronous JavaScript + XML

能够向服务器请求额外数据而无需卸载页面 => 改变 Web 诞生以来一直沿用的“单击,等待”的交互模式。


浏览器发起请求的几种方式(有什么区别):

  • 输入链接
  • src
  • ajax

浏览器获取数据的方式:

  • 请求
  • 本地存储(包括缓存(被动缓存)、loaclStorage、cookie等(主动缓存))

XHR 的出现将浏览器原生的通信能力提供给了开发人员。其实有很多中方法可以实现这种浏览器和服务器的通信:隐藏的框架或内嵌框架、Java Applet、Flash等

XMLHttpRequest 对象

XHR 的用法

new XMLHttpRequest()

function createXHR() {
    if (typeof XMLHttpRequest != "undefined") {
        return new XMLHttpRequest();
    } else if (typeof ActiveXObject != "undefined") {
        if (typeof arguments.callee.activeXString != "string") {
            var versions = ["MSXML2.XMLHttp.6.0", "MSXML2.XMLHttp.3.0", "MSXML2.XMLHttp"],
                i, len;

            for (i = 0, len = versions.length; i < len; i++) {
                try {
                    new ActiveXObject(versions[i]);
                    arguments.callee.activeXString = versions[i];
                    break;
                } catch (ex) {
                    //􏰋􏰌 跳过
                }
            }
        }
        return new ActiveXObject(arguments.callee.activeXString);
    } else {
        throw new Error("No XHR object available.");
    }
}
var xhr = createXHR();

xhr.open()

接受三个参数(要发送请求的类型(get/post等)、请求的URL、表示是否异步发送请求的布尔值)

xhr.open("get", "example.php", false);
// 1. URL 相对于执行代码的当前界面
// 2. 调用 open() 方法并不会真正发送请求,而只是启动一个请求以备发送

注意跨域

XHR.send()

接受一个参数(作为请求主体发送的数据,没有必须传入 null。因为这个参数对于有些浏览器是必须的。调用 send() 之后,请求就会被分配到服务器

xhr.send(null);

如果请求是同步的,那代码会等到服务器响应之后再继续执行。收到响应后,响应的数据会自动填充 XHR 对象的属性,相关属性有:

  • responseText:作为响应主体被返回的文本
  • responseXML:如果响应的内容类型是“text/xml”或“application/xml”,这个属性将保存包含这响应数据的 XML DOM 文档
  • status:相应的 HTTP 状态。一般用其判断请求结果,决定下一步的操作
  • statusText:HTTP 状态的说明。因为在跨浏览器使用此属性不可靠,因此不作为判断请求结果。

在接收到响应后,第一步是检查 status 属性,以确定响应已经成功返回。一般来说,可以将 HTTP 状态代码为 200 作为成功的标志。此时,responseText 属性内容已经就绪。此外,状态代码为 304 表示请求的资源并没有被修改,可以直接使用浏览器中缓存的版本。当然,也意味着响应是有效的:

xhr.open("get", "example.txt", false);
xhr.send(null);
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
    alert(xhr.responseText);
} else {
    alert("Request was unsuccessful: " + xhr.status);
}

如果请求是异步的(大多数情况下),此时可以检测 XHR 对象的 readyState 属性,该属性表示请求/响应过程的当前活动阶段。

  • 0:未初始化。尚未调用 open() 方法
  • 1:启动。已经调用了 open() 方法,但尚未调用 send() 方法
  • 2:发送。已经调用了 send() 方法,但尚未接收到响应
  • 3:接收。已经接收到部分响应数据
  • 4:完成。已经接收到全部响应数据,而且已经可以在客户端使用了

**只要 readyState 属性的值变化时,都会触发一次 readystatechange 事件。**可以利用这个事件来检测每次状态变化后 readyState 的值。不过必须在调用 open() 之前指定 readystatechange 事件处理程序才能确保跨浏览器兼容性。

var xhr = createXHR();
    xhr.onreadystatechange = function(){
        if (xhr.readyState == 4){   // 没有使用 this,避免产生错误
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
                  alert(xhr.responseText);
        } else {
             alert("Request was unsuccessful: " + xhr.status);
        }
    }
};
xhr.open("get", "example.txt", true);
xhr.send(null);

以上利用 DOM 0 级方法为 XHR 对象添加了时间处理程序,原因是并非所有浏览器都支持 DOM 2 级方法。

另外,在 接收到响应之前 还可以调用 abort() 方法来取消异步请求。

xhr.abort();

调用这个方法之后,XHR 对象会停止触发事件,而且也不再允许访问任何与响应有关的对象属性。在终止请求之后,还应该对 XHR 对象进行 解引用 操作。

由于内存原因,不建议重用 XHR 对象。

HTTP 头部信息

每个 HTTP 请求和响应都会带有响应的头部信息。

默认情况下,在发送 XHR 请求的同时,还会发送下列头部信息。

  • Accept:浏览器能够处理的内容类型
  • Accept-Charset:浏览器能够显示的字符集
  • Accept-Encoding:浏览器能够处理的压缩编码
  • Accept-Language:浏览器当前设置的语言
  • Connection:浏览器与服务器之间连接的类型
  • Cookie:当前页面设置的任何 Cookie
  • Host:发出请求的页面所在的域
  • Referer:发出请求的页面的 URI(HTTP规范写错了,正确应该是 referrer)
  • User-Agent:浏览器的用户代理字符串

setRequestHeader() 可以设置自定义的请求头部信息,接受两个参数:头部字段的名称和头部字段的值。要成功发送请求头部信息,必须在调用 open() 方法之后且调用 send() 方法之前调用 setRequestHeader():

var xhr = createXHR();
xhr.onreadystatechange = function () {
    if (xhr.readyState == 4) {
        if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
            alert(xhr.responseText);
        } else {
            alert("Request was unsuccessful: " + xhr.status);
        }
    }
};
xhr.open("get", "example.php", true);
xhr.setRequestHeader("MyHeader", "MyValue");
xhr.send(null);

服务器在接收到这种自定义的头部信息之后,可以执行响应的后续操作。尽量不要重写默认的头部信息,有的浏览器不允许这样做。

getResponseHeader() 方法并传入头部字段名称,可以取得相应的响应头部信息,而调用 getAllResponseHeaders() 方法则可以取得一个包含所有头部信息的长字符串。

var myHeader = xhr.getResponseHeader("MyHeader");
var allHeaders = xhr.getAllResponseHeaders();

GET 请求

GET 是最常见的请求类型,最常用于向服务器查询某些信息。必要时,可以将查询字符串参数追加到 URL 的末尾,以便将信息发送给服务器。对 XHR 而言,位于传入 open() 方法的 URL 末尾的查询字符串必须经过正确的编码才行。

使用 GET 请求经常会发生的一个错误,就是查询字符串的格式有问题。查询字符串中的每个参数的名称和值都必须使用 encodeURIComponent() 进行编码,然后才能放到 URL 的末尾;而且所有 名-值对儿 都必须由和号(&)分隔。

xhr.open("get", "example.php?name1=value1&name2=value2", true);
function addURLParam(url, name, value){
    url += (url.indexOf("?") == -1 ? "?" : "&");
    url += encodeURIComponent(name) + "=" + encodeURIComponent(value);
    return url;
}
var url = "example.php";

// 添加参数
url = addURLParam(url, "name", "Nicholas");
url = addURLParam(url, "book", "Professional Javascript");

// 初始化请求
xhr.open("get", url, false);

你能举出多少种使用 GET 请求的场景吗?

POST 请求

使用频次仅次于 GRT 的是 POST 请求,通常用于向服务器发送应该被保存的数据。POST 请求应该把数据作为请求的主体提交,而 GET 请求传统上不是这样。POST 请求的主体可以包含非常多的数据。

(GET VS POST 区别)

(?有种错觉是 POST 比 GET 更常用,是因为很多操作其实本质上都是 GET 请求,如:访问服务器的第一个请求(可能是直接访问,也可能是跳转,但 SPA 可能就第一个页面是 GET 请求,后面只是改变了 URL)、各种外部资源的引用(带有 src 属性的元素,img/link/script/audio/video/iframe/...)、主动发起的 GET 请求(Ajax 等)...)

xhr.open("post", "example.php", true);
xhr.send(); // 发送 POST 请求的第二步就是向 send() 方法传入某些数据

注意:

  1. Content-Type头部信息设置与服务端解析请求数据方法( $_POST / $HTTP_RAW_POST_DATA )
  2. POST 请求与 form 表单,以及如何用 POST 请求模拟表单提交
function submitData(){
    var xhr = createXHR();
    xhr.onreadystatechange = function(){
        if(xhr.readyState == 4){
            if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
                alert(xhr.responseText);
            }else{
                alert("Request was unsuccessful: " + xhr.status);
            }
        }
    }

    xhr.open("post", "postexample.php", true);
    xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    var form = document.getElementById("user-info");
    xhr.send(serialize(form));
}

XMLHttpRequest 2 级

FormData

表单数据的序列化 => XMLHttpRequest 2 级定义了 FormData 类型,可直接传入 XHR 的 send() 方法。

超时设定

XHR 对象的 timeout 属性,表示请求在等待响应多少毫秒之后就终止。超时规定事件内浏览器还没有接收到响应,就会触发 timeout 事件,进而会调用 onetimeout 事件处理程序。

var xhr = createXHR();
xhr.onreadystatechange = function(){
    if(xhr.readyState == 4){
        try{
            if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
                alert(xhr.responseText);
            }else{
                alert("Request was unsuccessful: " + xhr.status);
            }
        }catch(ex){
            // 假设由 ontimeout 事件处理程序处理
            // 当超时时,请求会自动终止,会调用 ontimeout 事件处理程序
            // 但此时 readyState 可能已经改变为 4 了 => 会调用 onreadystatechange 事件处理程序
            // 但在超时终止请求之后再访问 status 属性,会导致错误 => try-catch
        }
    }
}

xhr.open("get", "timeout.php", true);
xhr.timeout = 1000; // 将超时设置为 1 秒
xhr.ontimeout = function(){
    alert("Request did not return in a second.");
};
xhr.send(null);

overrideMimeType() 方法

重写 XHR 响应的 MIME 类型。

因为返回响应的 MIME 类型决定了 XHR 对象如何处理它,所以提供一种方法能够重写服务器返回的 MIME 类型很有用。但调用此方法必须在 send() 方法之前,才能保证重写响应的 MIME 类型。

var xhr = createXHR();
xhr.open("get", "text.php", true);
xhr.overrideMimeType("text/xml");
xhr.send(null);

进度事件

Poogress Event 规范定义了与客户端服务器通信有关的事件,后来也被其他 API 借鉴。

  • loadstart:在接收到响应数据的第一个字节时触发
  • progress:在接收响应期间持续不断地触发
  • error:在请求发生错误时触发
  • abort:在因为调用 abort() 方法而终止连接时触发
  • load:在接收到完整的响应数据时触发
  • loadend:在通信完成或者触发 error、abort 或 load 事件后触发

loadstart -> progress -> ... -> progress -> error/abort/load -> loadend

每个请求都从触发 loadstart 事件开始,接下来是一或多个 progress 事件,然后触发 error、abort 或 load 事件中的一个,最后以触发 loadend 事件结束。

load 事件理论是可替代 readystatechange 事件(没必要检查 readyState 属性了),但因为并非所有浏览器都为这个事件实现了适当的事件对象(event,其 target 属性指向 XHR 对象实例),所以还是需要使用 XHR 对象变量。

var xhr = createXHR();
xhr.onload = function(){    // 只要浏览器接收到服务器的响应,不管其状态如何,都会触发 load 事件。
    // 但必须要检查 status 属性,才能确定数据是否真的已经可用了
    if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
        alert(xhr.responseText);
    }else{
        alert("Request was unsuccessful: " + xhr.status);
    }
}
xhr.open("get", "altevents.php", true);
xhr.send(null);

progress 事件,会在浏览器接收新数据期间周期性触发。

onprogress 事件处理程序会接收到一个 event 对象,其 target 属性是 XHR 对象,但包含着三个额外的属性:

  • lengthComputable:表示进度信息是否可用的布尔值
  • position:已经接收的字节数
  • totalSize:根据 Content-Length 响应头部确定的预期字节数

创建进度指示器:

var xhr = createXHR();
xhr.onload = function(){
    if((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304){
        alert(xhr.responseText);
    }else{
        alert("Request was unsuccessful: " + xhr.status);
    }
}

xhr.onprogress = function(event){   // 必须在调用 open() 方法之前添加 onprogress 事件处理程序
    var divStatus = document.getElementById('status');
    if(event.lengthComputable){
        divStatus.innerHTML = "Received " + event.position + " of " + event.totalSize + " bytes";
    }
}

xhr.open("get", "altevents.php", true);
xhr.send(null);

跨域资源共享

何谓“跨域”?

通过 XHR 实现 Ajax 通信的一个主要限制,来源于跨域安全策略。默认情况下,XHR 对象只能访问与包含它的页面位于同一个域的资源(从字面上来看,很容易认为跨域是由服务端控制?但其实不然)。这种安全策略可以预防某些恶意行为,但是实现合理的跨域请求对开发某些浏览器应用程序也是至关重要的。

CORS(Cross-Origin Resource Sharing,跨域资源共享)是 W3C 的一个工作草案,定义了在必须访问跨源资源时,浏览器与服务器应该如何沟通。CORS 背后的基本**,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。(能否实现一个不受“同源策略”限制的浏览器?)

比如一个简单的使用 GET 或 POST 发送的请求,它没有自定义的头部,而主体内容是 text/plain,在发送改请求时,需要给它附加一个额外的 Origin 头部,其中包含请求页面的源信息(协议、域名和端口),以便服务器根据这个头部信息来决定是否给予响应。

Origin: http://www.nczonline.net

如果服务器认为这个请求可以接受,就在 Access-Control-ALlow-Origin 头部中回发相同的源信息(如果是公共资源,可以回发“*”

Access-Control-ALlow-Origin: http://www.nczonline.net

如果没有这个头部,或者有这个头部但源信息不匹配,浏览器就会驳回请求。

跨域 XHR 对象有一些限制(主要为了安全考虑):

  • 不能使用 setRequestHeader() 设置自定义头部
  • 不能发送和接收 cookie
  • 调用 getAllResponseHeaders() 方法总会返回空字符串

由于无论同源请求还是跨源请求都使用相同的接口,因此对于本地资源,最好使用相对 URL,在访问远程资源时再使用绝对 URL,这样做能消除歧义,避免出现限制访问头部或本地 cookie 信息等问题。

Prefighted Requests:CORS 通过一种叫做 Prefighted Requests 的透明服务器验证机制支持开发人员使用自定义的头部、GET 或 POST 之外的方法,以及不同类型的主体内容。

带凭据的请求:默认情况下,跨源请求不提供凭据(cookie、HTTP 认证及客户端 SSL 证明等)。通过将 withCredentials 属性设置为 true,可以指定某个请求应该发送凭据,并以下面的 HTTP 头部来响应:

Access-Control-Allow-Credentials: true

如果发送的是带凭据的请求,但是服务器的响应中没有包含这个头部,那么浏览器就不会把响应交给 JavaScript,于是:

  • responseText 为空字符串
  • status 为0
  • 还会调用 onerror() 事件处理程序

另外,服务器还可以在 Prefight 响应中发送这个 HTTP 头部,表示允许源发送带凭据的请求。

跨浏览器的 CORS:其实浏览器对 CORS 的支持成都并不都一样,但所有浏览器都支持简单的(非 Prefighte 和不带凭据的)请求。因此有必要实现一个跨浏览器的方案,检测 XHR 是否支持 CORS 的最简单方式,就是检查是否存在 withCredentials 属性,再结合检测 XDomainRequest 对象是否存在(IE),就可以兼顾所有浏览器了(?此处存疑,不知当前是什么情况)。

其他跨域技术

在 CORS 出现以前,要实现跨域 Ajax 通信颇费周折,开发人员想出一些办法,利用 DOM 中能够执行跨域请求的功能,在不依赖 XHR 对象的情况下也能发送某种请求。

虽然 CORS 技术已经无处不在,但开发人员自己发明的这些技术仍然被广泛使用,毕竟这样不需要修改服务器端代码。

图像 Ping

<img>标签。

图像 Ping是与服务器进行,简单、单向的跨域通信的一种方式。请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或 204 响应。通过图像 Ping,浏览器得不到任何具体的数据,但通过侦听 load 和 error 事件,它能知道响应是什么时候接收到的。

var img = new Image();
img.onload = img.onerror = function(){
    alert("Done!");
}
img.src = "http://www.example/com/test?name=Nicholas";

图像 Ping 最常用于跟踪用户点击页面或动态广告曝光次数。

图像 Ping 有两个主要的缺点:

  • 只能发送 GET 请求
  • 无法访问服务器的响应文本

因此,图像 Ping 只能用户浏览器与服务器间的单向通信。

JSONP

JSONP 是 JSON with padding(填充式 JSON 或参数式 JSON)的简写,是应用 JSON 的一种新方法。JSONP 看起来与 JSON 差不多,只不过 是被包含在函数调用中的 JSON

callback({"name": "Nicholas"});

JSONP 由两部分组成:

  • 回调函数:是当响应到来时应该在页面中调用的函数,回调函数的名字一般是在请求中指定的
  • 数据:是传入回调函数中的 JSON 数据
// 典型的 JSONP 请求
http://freegeoip.net/json?callback=handleResponse // 请求一个 JSONP 地理定位服务

通过查询字符串来指定 JSONP 服务的回调函数是很常见的,这里指定的回调函数的名字叫 handleResponse()。

JSONP 是通过动态<script>元素来使用的,使用时可以为 src 属性指定一个跨域 URL。(这里的 script 元素与 img 元素类似,都有能力不受限制地从其他域加载资源)

因为 JSONP 是有效的 JavaScript 代码,所以在请求完成后,即在 JSONP 响应加载到页面以后,就会立即执行。

function handleResponse(response){
    alert("You are at IP address " + response.ip + ", which is in " + response.city + ", " + response.region_name);
}
var script = document.createElement("script");
script.src = "http://freegeoip.net/json?callback=handleResponse";
document.body.insertBefore(script, document.body.firstChild);

JSONP 与图像 Ping相比,优点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。

不足在于:

  • JSONP 是从其他域中加载代码执行,很可能在响应中夹带一些恶意代码,因此在使用不是自己运维的 Web 服务时,一定得保证安全可靠
  • 要确定 JSONP 请求是否失败并不容易

Comet

更高级的 Ajax 技术(或者称之为“服务器推送”),Ajax 是一种从页面像服务器请求数据的技术,而 Comet 则是一种服务器向页面推送数据的技术。

实现方式:长轮询 和 流

服务器发送事件

SSE,Service-Sent Events

Web Sockets

目标是在一个单独的持久连接上提供全双工、双向通信。

纯文本 —— 复杂数据序列化

安全

对于未被授权系统有权访问某个资源的情况,称之为 CSRF(Cross-Site Request Forgery,跨站点请求伪造)。

为确保 XHR 访问的 URL 安全,通行的做法就是:

  • 要求以 SSL 连接来访问可以通过 XHR 请求的资源
  • 要求每一次请求都要附带经过相应算法计算得到的验证码

小结

Ajax 是无需刷新页面就能够从服务器取得数据的一种方法,关于 Ajax:

  • 负责 Ajax 运行的核心对象是 XMLHttpRequest(XHR)对象
  • 虽然各浏览器实现 XHR 存在差异,但基本用法还相对规范

同源策略是对 XHR 的一个主要约束,为通信设置了“相同的域,相同的端口,相同的协议”。

试图访问上述限制之外的资源,都会引发安全错误,除非采用被认可的跨域解决方案——CORS(大部分浏览器通过 XHR 对象原生支持 CORS),图像 Ping 和 JSONP 是另外跨域通信的技术。

《JavaScript高级程序设计(第3版)》学习笔记——三、变量、作用域和内存问题

三、变量、作用域和内存问题

对应“js高程ch4”

  • 变量和值
    • JavaScript 松散类型决定变量只是在特定时间用于保存特定的一个名字而已。
    • 值可分为两种类型:
      • 基本类型值在内存中占据固定大小的空间,保存在栈内存中
      • 引用类型值是对象,保存在堆内存中(a.可以动态改变属性)。保存引用类型值的变量实际上保存的是一个指向该对象的指针,而不是对象本身。(b.具体差异在值复制过程中表现尤为明显。)
      • vs
        • a. 动态的属性
        • b. 复制变量值(浅拷贝)
        • c. 传递参数
          • 都是按值传递
          • 在向参数传递:
            • 基本类型值时,被传递的值会被复制给一个局部变量(即命名参数 / arguments 对象中的一个元素)。
            • 引用类型值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。(但不是按引用传递,因为按引用传递修改局部对象引用并不会影响外部对象的引用)
      • 确定一个值(类型检测):
        • 是哪种基本类型:typeof

复制基本类型值:
default

复制引用类型值:
default

  • 执行环境及作用域

    • 执行环境(execution context),定义了变量或函数有权访问的其他数据,决定了它们各自的行为。
    • 每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。
    • 执行环境可分为全局执行环境(也称为全局环境)和函数执行环境之分。
      • 全局执行环境:ECMAScript 实现所在的宿主环境不同,表示执行环境的对象也不同。,在 Web 浏览器中全局执行环境 -> window
      • 函数执行环境:当执行流进入一个函数时,函数的环境就会被推入一个环境栈中
    • 某个执行环境中的所有代码执行完毕后(全局执行环境 -> 应用程序退出或关闭网页;函数执行环境 -> 函数执行完毕),该环境被销毁,保存在其中的所有变量和函数定义也会随之销毁
    • 当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。用以保证对执行环境有权访问的所有变量和函数的有序访问。
      • 作用域链的前端,始终都是当前执行代码所在环境的变量对象
        • 如果这个环境是函数,则将其活动对象作为变量对象,最开始只包含一个变量,即 arguments 对象
      • 作用域链的最后一个,始终都是全局执行环境的变量对象
    • 延长作用域链
      • 虽然执行环境的类型只有两种——全局和局部(函数),但是有些语句可以延长作用域链,语句执行结束被移除:
        • try-catch 语句的 catch 块:会创建一个新的变量对象,其中包含的是被抛出的错误对象的声明
        • with 语句:会将指定的对象添加在作用域链中
      • 因为 js 中没有块级作用域,所以在 catch 语句块和 with 语句块中定义的变量或函数和外部定义是没有差别的(特殊!!!)
    • 没有块级作用域
      • 声明变量
        • 使用 var 声明的变量会自动添加到最接近的环境中:
          • 在函数内部 -> 函数的局部环境
          • 在 with 语句中 -> with 语句所在环境
        • 如果初始化变量时没有使用 var 声明,该变量会自动添加到全局环境中(不推荐)
      • 查询标识符:向上逐级查询
  • 自动垃圾收集

    • 实现策略
      • 标记清除(常用,推荐)
      • 引用计数(少用,问题多):存在循环引用风险,导致无法回收
    • 性能问题:垃圾收集的时间间隔
    • 管理内存:为执行中的代码只保存必要的数据。一旦数据不再有用,最好通过将其值设为 null 来释放其引用——解除引用,以脱离执行环境(离开作用域的值将被自动标记为可以回收),这样垃圾收集器下次运行时将其回收。适用于全局变量、全局对象的属性以及循环引用变量的引用。(局部变量会在环境销毁时随之销毁)
function createPerson(name){
    var localPerson = new Object();
    localPerson.name = name;
    return localPerson;
}
var globalPerson = createPerson("Nicholas");
// 手工接触 globalPerson 的引用
globalPerson = null;

《JavaScript高级程序设计(第3版)》学习笔记——十五、JSON

JSON

JavaScript Object Notation,JavaScript 对象表示法。是一种数据格式。

JSON 是 JavaScript 的一个严格的子集,利用了 JavaScript 中的一些模式来表示结构化数据。

语法

JSON 的语法可以表示以下三种类型的值:

  • 简单值,使用与 JavaScript 相同的语法,可以在 JSON 中表示字符串、数值、布尔值和 null,但不支持 JavaScript 中的特殊值 undefined。
  • 对象,复杂数据类型,表示的是一组无序的键值对儿。每个键值对儿中的值可以是简单值,也可以是复杂数据类型的值。
  • 数组,复杂数据类型,表示的是一组有序的键值对儿,可以通过数组索引来访问其中的值,数组的值也可以是任意类型——简单值、对象或数组。

简单值

5
true
null
"Hello, World!"

JSON 字符串必须使用双引号。

对象

JSON 中对象的属性需要加引号。

{
    "name": "Nicholas",
    "age": 30
}

数组

[
    25,
    "hi",
    true
]
[
    {
        "title": "Professional JavaScript",
        "author": "Nicholas C. Zakas",
        "edition": 1
    },
    {
        "title": "Professional JavaScript",
        "author": "Nicholas C. Zakas",
        "edition": 1
    },
    {
        "title": "Professional JavaScript",
        "author": "Nicholas C. Zakas",
        "edition": 1
    },
    {
        "title": "Professional JavaScript",
        "author": "Nicholas C. Zakas",
        "edition": 1
    }
]

对象和数组通常是 JSON 数据格式的最外层形式,利用它们能够创造各种各样的数据结构。

解析和序列化

JSON 数据结构 能够很方便的解析为 有用的 JavaScript 对象。

JSON 对象(不支持的浏览器,可以使用一个 shim:https://github.com/douglascrockford/JSON-js)

JavaScript术语:shim 和 polyfill

ECMAScript 5 定义了一个原生的 JSON 对象,可以用来将对象序列化为 JSON 字符串或者将 JSON 数据解析为 JavaScript 对象。JSON 对象有两个方法分别用来实现上述两项功能,这两个方法都有一些选项,通过它们可以改变过滤的方法,或者改变序列化的过程。

stringify()

把 JavaScript 对象序列化为 JSON 字符串。

var book = {
    title: "Professional JavaScript",
    authors: [
        "Nicholas C. Zakas"
    ],
    edition: 3,
    year: 2011
}
var jsonText = JSON.stringify(book);

默认情况下,输出的 JSON 字符串不包含任何空格字符或缩进。

jsonText 中的字符串:

{"title":"Professional JavaScript","authors":["Nicholas C. Zakas"],"edition":3,"year":2011}

在序列化 JavaScript 对象时,所有函数及原型成员都会被有意忽略,不体现在结果中。值为 undefined 的任何属性也都会被跳过。结果中最终都是值为有效 JSON 数据类型的实例属性。

JSON.stringify() - JavaScript | MDN

// JSON.stringify(value[, replacer[, space]])

// 1. 过滤结果
// 数组
var book = {
    title: "Professional JavaScript", 
    authors: [
        "Nicholas C. Zakas"
    ],
    edition: 3,
    year: 2011
};
var jsonText = JSON.stringify(book, ["title", "edition"]);
// jsonText
// {"title":"Professional JavaScript","edition":3}

// 函数
var jsonText = JSON.stringify(book, function(key, value){
    switch(key){
        case "authors":
            return value.join(',');
        case "year":
            return 5000;
        case "edition":
            return undefined;
        default:
            return value;
    }
}); 
// jsonText
// {"title":"Professional JavaScript","authors":"Nicholas C. Zakas","year":5000}


// 2. 字符串缩进
var jsonText = JSON.stringify(book, null, 4);
// jsonText
// {
//     "title": "Professional JavaScript",
//     "authors": [
//         "Nicholas C. Zakas"
//     ],
//     "edition": 3,
//     "year": 2011
// }

var jsonText = JSON.stringify(book, null, " - -");
// jsonText
// {
//  - -"title": "Professional JavaScript",
//  - -"authors": [
//  - - - -"Nicholas C. Zakas"
//  - -],
//  - -"edition": 3,
//  - -"year": 2011
// }

toJSON() 方法:有时候,JSON.stringify() 还是不能满足对某些对象进行自定义序列化的需求。=>在这些情况下,可以给对象定义 toJSON() 方法,返回其自身的 JSON 数据格式。

var book = {
    title: "Professional JavaScript", 
    authors: [
        "Nicholas C. Zakas"
    ],
    edition: 3,
    year: 2011,
    toJSON: function(){
        return this.title;
    }
};
var jsonText = JSON.stringify(book);
// jsonText
// "Professional JavaScript"

可以让 toJSON() 方法返回任何值,如果返回 undefined => 1. 如果包含它的对象嵌入在另一个对象中,会导致它的值变成 null, 2. 如果它是顶级对象,那序列化返回的结果是 undefined。

toJSON() 作为函数过滤器的补充,理解 序列化(JSON.stringify)的内部顺序 十分重要:

  1. 如果存在 toJSON() 方法而且能通过它取得有效的值,则调用该方法。否则,返回对象本身;
  2. 如果提供第二个参数,应用这个函数过滤器,传入函数过滤器的值是第 1 步返回的值;
  3. 对第 2 步返回的每个值进行相应的序列化;
  4. 如果提供第三个参数,执行相应的格式化。

parse()

把 JSON 字符串解析为原生 JavaScript 值。将 JSON 字符串直接传递给 JSON.parse() 就可以得到相应的 JavaScript 值。但注意:序列化前和解析后的对象是相互独立、没有任何关系的对象。如果传给 JSON.parse() 的字符串不是有效的 JSON,会抛出错误(一般会结合 try-catch)。

JSON.parse() - JavaScript | MDN

var book = {
    title: "Professional JavaScript", 
    authors: [
        "Nicholas C. Zakas"
    ],
    edition: 3,
    year: 2011,
    releaseDate: new Date(2011, 11, 1)
};
var jsonText = JSON.stringify(book);

var bookCopy = JSON.parse(jsonText, function(key, value){
    if(key == "releaseDate"){
        return new Date(value);
    }else{
        return value;
    }
});
alert(bookCopy.releaseDate.getFullYear());  // 2011

20180620-技术点收集

  • scroll-view局部滚动的实现方式
  • 支付宝小程序showToast封装,hideToast
  • node-sass替代sass(ruby)

在React中应用Swiper懒加载效果,更换数据源后图片显示未更新 bug

React中利用Swiper实现轮播+懒加载

在实现轮播效果的时候,往往会用到一些库,例如比较强大的[Swiper](http://3.swiper.com.cn/)。但是如果要基于React实现的话,往往会第一时间想去找找是否有相应的组件库支持,例如antd[Carousel组件](https://mobile.ant.design/components/carousel-cn/),但是在使用过程中,还是会遇到各种各样的bug,这里暂时不展开。最终,采用基于swiper在react中实现轮播效果。

轮播

  • componentDidMount后
if(this.indexSwiper){
    this.indexSwiper.destroy(true,true);
    this.indexSwiper = null;
}
this.indexSwiper = new Swiper('.banner-recommend', {
    loop: true,
    autoplay: 3000,
    speed: 800,
    autoplayDisableOnInteraction: false,
    pagination: '.banner-recommend .swiper-pagination',
    onInit: (swiper) => {
        if(this.state.bannerList.length > 1){
            $(".banner-recommend .swiper-pagination").show();
        }else{
            swiper.stopAutoplay();
            swiper.params.onlyExternal = true;
            $(".banner-recommend .swiper-pagination").hide();
        }
    }
});
  • JSX
<section className="swiper-container banner-recommend">
    <div className="swiper-wrapper">
        {
            bannerList.map((banner) =>
                <section className="banner-item swiper-slide">
                    <img src={banner.pictureUrl} alt=""/>
                </section>
            )
        }
    </div>
    <div className="swiper-pagination"></div>
</section>

懒加载

根据Swiper文档,http://3.swiper.com.cn/api/Images/2015/0308/213.html,加上相应属性如下:

  lazyLoading: true
<section className="banner-item swiper-slide" >
  <section className="swiper-lazy" data-background={banner.pictureUrl}></section>
</section>

出现bug

我们知道React是数据驱动的,而懒加载效果除了依赖数据外,还依赖具体的触发条件的,例如首次出现在屏幕中,具体的实现方式一般都是设置一个data-src属性,当达到触发条件时,将元素的src属性赋值为data-src的值。但是在React中,如果修改状态,仅仅会修改data-src属性值,并不能修改能够真正决定显示的src属性的值,这样就会出现当修改了轮播的数据,页面上显示的轮播图片并没有改变的bug,但其实data-src的值已经更新了。

解决方案

在更新banner数据时,先将其置空,以达到想要的初始化效果。

this.setState({
    bannerList: {}, // 解决修改banner源与懒加载冲突bug
}, ()=>{
    that.setState({
        bannerList: getData.content || []
    }, ()=>{
        that.initBanner();
        // **此处隐藏一个bug**,具体说明见楼下
    });
});

《JavaScript高级程序设计(第3版)》学习笔记——十四、错误处理与调试

错误处理与调试

浏览器报告的错误

错误处理

try-catch

try{
    // 可能会导致错误的代码
} catch(error){
    // 在错误发生时怎么处理
}

如果在 try 块中的任何代码发生了错误,就会立即退出代码执行过程,然后接着执行 catch 块。此时 catch 块会接收到一个包含错误信息的对象。

finally 子句

无论如何都会执行(无论 try 或 catch 语句块中包含什么代码——甚至 return 语句)

function testFinally(){
    try{
        return 2;   // 会被忽略
    } catch(error){
        return 1;
    } finally {
        return 0;   // 函数最终返回 0
    }
}
  • 使用了 finally,catch 就可选
  • 使用了 finally,try 和 catch 中的 return 语句就会被忽略
错误类型
  • Error:基类型,主要目的是供开发人员抛出自定义错误
  • EvalError:
  • RangeError
  • ReferenceError
  • SyntaxError
  • TypeError
  • URIError

抛出错误

throw,用于随时抛出自定义错误。在遇到 throw 操作符时,代码会立即停止执行(其他语言也可能会这样吗?)。仅当有 try-catch 语句捕获到被抛出的值时,代码才会继续执行。

  • 通过抛出错误类型的实例对象,模拟浏览器错误
  • 继承 Error 创建自定义错误类型
function CustomError(message){
    this.name = 'CustomError';
    this.message = message;
}

CustomError.prototype = new Error();

throw new CustomError('My message');

要针对函数为什么会执行失败给出更多信息(=>便于调试,查找错误来源),抛出自定义错误是一种很方便的方式。应该在出现某种特定的已知错误条件,导致函数无法正常执行时抛出错误。换句话说,浏览器会在某种特定的条件下执行函数时抛出错误。

开发过程中,重点关注函数和可能导致函数执行失败的因素,良好的错误处理机制应该可以确保代码中只发生你自己抛出的错误。

抛出错误(throw)与捕获错误(try-catch):

  • 捕获错误的目的在于避免浏览器以默许方式处理它们,只应该捕获那些你确切地知道该如何处理的错误
  • 抛出错误的目的在于提供错误发生具体原因的消息

错误(error)事件

任何没有通过 try-catch 处理的错误都会触发 window 对象的 error 事件。在任何 web 浏览器中,onerror 时间处理程序都不会创建 event 对象,但它可以接收三个参数:错误消息、错误所在的 URL 和行号。

要指定 onerror 时间处理程序,必须使用如下所示的 DOM 0 级技术:

window.onerror = function(message, url, line){
    alert(message);
}

只要发生错误,无论是不是浏览器生成的,都会触发 error 事件,并执行这个事件处理程序。然后,浏览器默认的机制发挥作用,像往常一样显示出错误信息。可以在事件处理程序中返回 false,可以阻止浏览器报告错误的默认行为。

window.onerror = function(message, url, line){
    alert(message);
    return false;   // 这样实际上就充当了整个文档中的 try-catch 语句,可以捕获所有无代码处理的运行时错误
}

图像页支持 onerror 事件,只要图像的 src 特性中的 URL 不能返回可以被识别的图像格式,就会触发 error 事件。

处理错误的策略

记录和监控系统 => 分析错误模式,追查错误原因,同时帮助确定错误会影响到多少用户。

常见的错误类型

错误处理的核心,是首先要知道代码里会发生什么错误。

由于 JavaScript 是松散类型的,而且也不会验证函数的参数,因此错误只会在代码运行期间出现。(why?)

一般来说,需要关注三种错误:

  • 类型转换错误
  • 数据类型错误
  • 通信错误

以上错误会 在特定的模式下 或者 没有对值进行足够的检查的情况下发生。

类型转换错误:发生在使用某个操作符,或者使用其他可能会自动转换值的数据类型的语言结构时。

数据类型错误:松散类型 => 在使用变量和函数参数之前,不会对它们进行比较以确保它们的数据类型正确。

通信错误:随着 Ajax 编程的兴起 => Web APP 在其生命周期内动态加载信息或功能。与服务器之间的任何一次通信,都有可能会产生错误。

区分致命错误和非致命错误

非致命错误

  • 不影响用户的主要任务
  • 只影响页面的一部分
  • 可以恢复
  • 重复相同操作可以消除错误

致命错误

  • 应用程序根本无法继续运行
  • 错误明显影响到了用户的主要操作
  • 会导致其他连带错误

把错误记录到服务器

function logError(sev, msg){
    var img = new Image();
    img.src = "log.php?sev=" + encodeURIComponent(sev) + "&msg=" + encodeURIComponent(msg);
}
  • 所有浏览器都支持 Image 对象,包括那些不支持 XMLHttpRequest 对象的浏览器
  • 可以避免跨域限制
  • 在记录错误的过程中出问题的概率比较低

只要是使用 try-catch 语句,就应该把相应错误记录到日志中。

调试技术

将消息记录到控制台

  • console.error(message):将 错误消息 记录到控制台
  • console.info (message):将 信息性消息 记录到控制台
  • console.log (message):将 一般消息 记录到控制台
  • console.warn (message):将 警告消息 记录到控制台

将消息记录到当前页面

function log(message) {
    var console = document.getElementById("debuginfo");
    if (console === null) {
        console = document.createElement("div");
        console.id = "debuginfo";
        console.style.background = "#dedede";
        console.style.border = "1px solid silver";
        console.style.padding = "5px";
        console.style.width = "400px";
        console.style.position = "absolute";
        console.style.right = "0px";
        console.style.top = "0px";
        document.body.appendChild(console);
    }
    console.innerHTML += "<p>" + message + "</p>";
}

抛出错误

function assert(condition, message){
    if (!condition){
        throw new Error(message);
    }
}

function divide(num1, num2){
    assert(typeof num1 == "number" && typeof num2 == "number",
           "divide(): Both arguments must be numbers.");
    return num1 / num2;
}

小结

避免浏览器响应 JavaScript 错误的方法:

  • 在可能发生错误的地方使用 try-catch,有机会以适当的方式对错误给出响应,而不必沿用浏览器处理错误的机制
  • 使用 window.onerror 时间处理程序,这种方式可以接受 try-catch 不能处理的所有错误

另外,对任何 Web 应用程序都应该分析可能的错误来源,并制定处理错误的方案:

  • 首先,必须要明确致命错误和非致命错误
  • 其次,再分析代码,以判断最可能发生的错误。主要原因有:
    • 类型转换
    • 未充分检测数据类型
    • 发送给服务器或从服务器接收到的数据有错误

bat 命令了解一下

现在每天都会做的一件事情,就是抓取早读课的最新推送,然后 push 到 FeZaoDuKe-Collection 仓库中。

做法其实很傻,就是手动在 Node 环境中运行负责“抓取”的 js 文件,存到本地。然后再次手动使用 Git 命令提交。

其实,操作很简单,但是就想着能不能更简单一点,终极目标是:早读课文章推送后,立马能够自动同步到我的仓库中

但是,里面具体的很多细节还有待研究(虽然大神一下就能实现,这里先留个坑,有时间再来解决,主要是定时任务这块还不知道怎么实现)

“自动抓取”和“自动提交”通过以下 bat 命令实现:

// 自动抓取
node getToDayNews.js

// 自动提交
git add README.md
git commit -m ":memo:update"
git push origin master

这里,执行 git push 命令时可能每次都会弹出需要登录的提示或者直接超时报错,可以采用 RSA 的方式保持登录态。

《JavaScript高级程序设计(第3版)》学习笔记——四、引用类型

引用类型

  • 引用类型

    • 引用类型是一种数据结构,用于将数据和功能组织在一起。
    • 引用类型也常被称为“类”,但是并不妥当,因为 ECMAScript 不具备传统面向对象语言所支持的类和接口等基本结构。
    • 引用类型也常被称为“对象定义”,因为描述的是一类对象所具有的属性和方法。
  • 引用的值(即对象)是引用类型的实例。

    • 使用new操作符后跟一个构造函数来创建。
    • 构造函数本身就是一个函数,只不过该函数是出于创建新对象的目的而定义的函数
  • Object 类型

    • 创建 Object 实例的方法
      • 使用new操作符后跟 Object 构造函数
      • 对象字面量
        • 不会调用 Object 构造函数
        • 属性名可以使用字符串(属性名可包含会导致语法错误的字符,或者关键字和保留字)
        • 一般函数通常使用对象字面量来封装多个可选参数,对必需值直接使用命名参数
    • 访问对象属性
      • 点表示法
      • 方括号
        • 属性需要使用字符串访问
        • 可以通过变量访问属性
  • Array 类型

    • 创建数组实例的方法

      • 使用 Array 构造函数
      • 数组字面量表示法
        • 不会调用 Array 构造函数
    • 数组大小

      • 动态调整
        • arr[arr.length] = xxx; // 往数组末尾新添项
    • 数组检测

      • arr instanceof Array; // 当有多个全局环境时存在不同版本的 Array 构造函数
      • Array.isArray(arr)
    • 转换方法

      • valueOf() -> 数组本身
      • toString() -> 数组的字符串表示(每个值的字符串表示拼接成了一个字符串,中间用逗号分隔)(此方法一般隐式调用居多)
      • toLocaleString() -> 一般同 toString()
      • join() -> 可以使用不同的分隔符来构建字符串(默认(=>不传或传入undefined)情况会用逗号分隔,传入空字符串才无分隔)
      • 类型转换有一种情况是某些函数接受特定类型的参数,会将参数转换为对应类型
    • 栈方法(LIFO)

      • push():可以接收任意数量的参数,逐个添加到数组末尾,并返回修改后的长度
      • pop():从数组末尾移除最后一项,同时减少数组的 length 值,并返回移除的项
    • 队列方法(FIFO)

      • shift():移除数组的第一项,并返回该项,同时数组长度减1
      • unshift():在数组前端添加任意个项并返回新数组的长度
    • 重排序方法

      • reverse():逆置,返回排序后的数组(影响原始数组)
      • sort():按升序排列数组的项。会调用每个数组项的 toString() 方法再排序(即使是数值项),返回排序后的数组(影响原始数组)
        • 默认排序方式并不理想:可以理解为默认的sort()传入了一个比较函数,会将相邻项先做toString处理,再通过字符串比较的方法升序排列
        • 可接收一个比较函数作为参数
          • (a, b)
            • <0,a比b“小”:a在b前(此时位置不变)
            • =0,a和b“相等”:位置不变
            • 0,a比b“大”:b在a前(换位置)

          • a和b传入到比较函数,根据“升序”排列
          • 对于数值类型/valueOf()方法会返回数值类型的对象类型,可以直接减法
        • sort()本质上是使用了什么排序算法?类似“冒泡”?https://segmentfault.com/q/1010000008506262
    • 操作方法

      • concat():会先创建当前数组的一个副本,然后将接收到的参数添加到这个副本的末尾,最后返回新构建的数组。(不影响原始数组)
        • 不传递参数:返回当前数组的一个副本
        • 传递多个数组参数:每个数组的数组项逐一添加
        • 传递的不是数值:直接将其作为一项添加到数组末尾
      • slice():基于当前数组中的一项或多项创建一个新数组。(不影响原始数组)由参数决定:
        • 传入一个参数:从该位置开始到当前数组末尾的所有项
        • 传入两个参数:从第一个参数位置(包含)开始到第二个参数位置(不包含)的项
          • 如果两个参数相同,则返回空数组(亲测)
        • 参数可为负数:位置 = 数组长度 + 负的参数
      • splice():强大的数组方法,可最多传入2+x个参数,分别是:起始位置(1), 要删除的项数(1), 要插入的项<逐一传入>(n),根据不同传入情况可实现(影响原始数组):
        • 删除:可以删除从任意位置开始任意项,只传入前两个参数
        • 插入:可以在任意位置插入任意项,参数均传,其中第二个参数为0(删除项数为0)
        • 替换:可以从任意位置替换任意几项为任意项,参数均传,其中第二个参数不为0
            var colors = [“red”, “green”, “blue”];
            var removed = colors.splice(0,1); //remove the first item
            alert(colors); //green,blue
            alert(removed); //red - one item array
            removed = colors.splice(1, 0, “yellow”, “orange”); //insert two items at position 1
            alert(colors); //green,yellow,orange,blue
            alert(removed); //empty array
            removed = colors.splice(1, 1, “red”, “purple”); //insert two values, remove one
            alert(colors); //green,red,purple,orange,blue
            alert(removed); //yellow - one item array
    • 位置方法

      • 两个位置方法,均接收两个参数:要查找的项、查找起始位置(可选,可为负数)
        • indexOf():从前往后
        • lastIndexOf():从后往前
      • 返回要查找的项在数组中的位置,没有为-1
      • 比较方式为“全等比较===”
    • 迭代方法

      • 五个迭代方法,均接收两个参数:要在每一项上运行的函数、运行该函数的作用域对象(可选)。对数组中的每一项运行给定函数:
        • every():如果该函数对每一项都返回true,则返回true
        • filter():返回该函数会返回true的项组成的数组
        • forEach():没有返回值
        • map():返回每次函数调用的结果组成的数组
        • some():如果该函数对任一项返回true,则返回true
        var numbers = [1,2,3,4,5,4,3,2,1];
        var everyResult = numbers.every(function(item, index, array){
            return (item > 2);
        });
        alert(everyResult); //false
        
        var someResult = numbers.some(function(item, index, array){
            return (item > 2);
        });
        alert(someResult); //true
        
        var numbers = [1,2,3,4,5,4,3,2,1];
        var filterResult = numbers.filter(function(item, index, array){
            return (item > 2);
        });
        alert(filterResult); //[3,4,5,4,3]
        
        var numbers = [1,2,3,4,5,4,3,2,1];
        var mapResult = numbers.map(function(item, index, array){
            return item * 2;
        });
        alert(mapResult); //[2,4,6,8,10,8,6,4,2]
        
        var numbers = [1,2,3,4,5,4,3,2,1];
        numbers.forEach(function(item, index, array){
            //do something here
        });
    • 归并方法

      • 两个归并方法,都会迭代数组的所有项,然后构建一个最终返回的值。均接收两个参数:在每一项上调用的函数、作为归并基础的初始值(可选)
        • reduce():从前往后
        • reduceRight():从后往前
      • 第一个参数函数接收4个参数:前一个值、当前值、项的索引、数组对象。函数返回的任何值都会作为第一个参数自动传给下一项
      var values = [1,2,3,4,5];
      var sum = values.reduce(function(prev, cur, index, array){
          return prev + cur;
      });
      alert(sum); //15
      
      var values = [1,2,3,4,5];
      var sum = values.reduceRight(function(prev, cur, index, array){
          return prev + cur;
      });
      alert(sum); //15
  • Date类型

    • 创建一个日期对象:使用Date构造函数
      • 不传入参数 => 自动获得当前日期和时间
      • 传入该日期的毫秒数(从UTC时间1970年1月1日零时开始经过的毫秒数),毫秒数获取方法
        • Date.parse():接收一个表示日期的字符串参数(ECMA-262并没有定义应该支持哪些日期格式,因此会因实现而异,通常表现为因地区而异),然后尝试根据这个字符串返回相应日期的毫秒数。否则返回NaN。通常此过程在构造函数中可自动转化
        • Date.UTC()
    • Date.now():返回调用这个方法时的日期和时间的毫秒数。使用+操作符把Date对象转换为字符串
    • 继承的方法
      • toLocaleString()
      • toString()
      • valueOf():返回日期的毫秒表示 => 可以直接使用比较操作符来比较日期值
    • 日期格式化方法和日期/时间组件方法(复杂)
  • RegExp类型

    • 图形化正则表达式工具:https://regexper.com/
    • 创建正则表达式
      • 字面量
      • 构造函数
    • RegExp实例属性
      • global
      • ignoreCase
      • lastIndex
      • multiline
      • source
    • RegExp实例方法
      • exec()
      • test()
      • 继承的toString()和toString()会返回正则表达式的字面量
    • RegExp构造函数属性
      • “静态属性”
  • Function类型

    • 每个函数本质上都是Function类型的实例(即对象) => 函数本质是对象。
      • 引申问题:在js中,类型不也是对象吗?
    • 函数名实际上只是一个指向函数对象的指针(变量名) => 函数名本质是指针变量。因此函数名其实和其他包含对象指针的变量没有什么不同
      • 函数名和函数本身没有绑定 --> js没有重载
      • 所有引用类型值不都是指针吗?理解里最初C语言定义的指针就是保存地址的变量
    • 创建函数(函数定义):
      • 函数声明
      • 函数表达式
      • 使用Function构造函数(不推荐)
    • 函数声明 vs 函数表达式
      • 解析器在向执行环境中加载数据时,会先读取函数声明,使其在执行任何代码之前可用。
      • 即在代码执行之前,,解析器会通过函数声明提升,function declaration hoisting,读取并将函数声明添加到执行环境中。
    • 作为值的函数
      • 函数名本身就是变量,所以函数也可以作为值来使用
        • 函数作为参数传递
        • 函数作为值返回
    • 函数内部属性(并不是函数的属性)
      • arguments:类数组对象,主要用于保存函数参数
        • arguments.callee:指向拥有这个arguments对象的函数 => 解除函数名与函数紧耦合
        • this:引用的是函数据以执行环境对象
          • 在调用函数前(执行),this的值并不确定
          • 但是无论this的值是什么,调用的函数 因为函数名仅仅是一个变量而已,因此无论在哪个环境中执行,都是指向的同一个函数。
    • 函数对象属性
      • caller:调用当前函数 的 函数的引用(在全局作用域下值为null)
    • 函数的属性与方法
      • length:函数希望接收的命名参数的个数
      • prototype:
        • 对于ECMAScript引用类型而言,prototype是保存它们所有实例方法的真正所在。
        • toString()和valueOf()等方法,实际上都保存在prototype名下,只不过通过各自的对象实例访问。(函数其实都是同一个)
        • 在创建自定义引用类型和实现继承时,prototype属性的作用是极其重要的。
        • 在ECMAScript 5 中,prototype属性是不可枚举的,因此使用for-in无法发现。
      • 非继承而来的方法(函数类型才有的方法,Object类型没有):
        • apply()和call(),作用都是在特定的作用域中调用函数,即相当于设置函数体内this对象的值。
        • 真正强大之处在于:扩充函数赖以运行的作用域。最大好处在于:对象不需要与方法有任何耦合关系
        • 均接收两种参数:一个是在其中运行函数的作用域,即this,另一个是传递的参数。两者区别在于第二个参数传递的方式不同,前者是参数数组(也可以是arguments对象),后者是参数的逐一传入
      • bind():会创建一个函数的实例,其中this值会被绑定到传给bind()函数的参数值
    • 继承方法
      • toString()、toLocaleString()、valueOf()返回函数代码,表现各异
  • 基本包装类型

    • 为了能(更方便)操作基本类型值,ECMAScript还提供了3个特殊的引用类型:Boolean、Number、String
    • 实际上,每当读取一个基本类型值时,后台就会创建一个对应的基本包装类型的对象,从而能够调用一些方法来操作这些数据,见下面代码演示:
    // 对于这段代码
    var s1 = 'test';
    var s2 = s1.substring(2);
    
    // 其中第2句在执行过程中,对应可以细分为:
    var sTemp = new String('test'); // 创建String类型的一个实例
    var s2 = sTemp.substring(2);    // 在实例上调用指定的方法
    sTemp = null;   // 销毁这个实例
    • 引用类型与基本包装类型的主要区别就是:对象的生存期
      • 使用new操作符创建的引用类型的实例,在执行流离开当前作用域之前都一直保存在内存中。
      • 自动创建的基本包装类型的对象,则只存在那行代码的执行瞬间,然后立即销毁。 => 不能在运行时为基本包装类型值添加属性和方法。
    • Object构造方法也会像工厂方法一样,根据传入值的类型返回相应基本包装类型的实例。例:new Object('test'),传入null和undefined会返回空对象
    • 使用new调用基本包装类型的构造函数(返回相应的实例对象,理论上不支持这种方式,因为容易混淆处理的是基本类型值还是引用类型值,导致出现bug) 与 直接调用同名的转型函数(返回基本类型值) 是不一样的。
    • Boolean类型
      • 重写了valueOf()(返回true/false)
      • 重写了toString()和toLocaleString()(返回"true"/"false")
    • Number类型
      • 重写了valueOf()、toString()和toLocaleString()
      • toFixed()
      • toExponential()
      • toPrecision()
    • String类型
      • 写了valueOf()、toString()和toLocaleString()
      • length属性
      • 字符方法
        • charAt()
        • charCodeAt()
        • 使用方括号加数值索引来访问字符串中的特定字符
      • 字符串方法
        • concat():可接收多个参数,不影响原始字符串值。 => 拼接字符串,在实际中更多使用**+号**
        • 基于子字符串创建新字符串,不影响原始字符串值
          • slice()
          • substr()
          • substring()
        • 字符串位置方法
          • indexOf()
          • lastIndexOf()
        • trim()方法,不影响原始字符串值
          • trim()
          • trimLeft()
          • trimRight()
        • 字符串大小写转换方法
          • toLowerCase()、toLocalLowerCase()
          • toUpperCase()、toLocalUpperCase()
        • 字符串的模式匹配方法,等同于RegExp中的exec()方法
          • match()
          • search()
          • replace()
          • htmlEscape()
          • split()
        • localCompare()
        • 静态方法fromCharCode()
        • HTML方法(现在基本不用)
  • 单体内置对象

    • 什么是内置对象?
      • 由ECMAScript实现提供的、不依赖于宿主环境的对象,这些对象在ECMAScript程序执行之前就已经存在了,例如Object、Array、String等
      • 不必实例化内置对象,因为它们已经实例化了
    • 两个单体内置对象:Global和Math
      • Global(全局)对象,理论上不属于任何其他对象的属性/方法最终都是Global的属性/方法,例如isNaN()、isFinite()、parseInt()、parseFloat()等
        • 还有:
          • URL编码方法
            • encodeURI()、encodeURIComponent()
            • decodeURI()、decodeURIComponent()
          • eval():“代码注入”风险
        • Global对象的属性
          • 特殊的值:undefined、NaN、Infinity
          • 所有原声引用类型的构造函数
        • window:ECMAScript没有指出如何直接访问Global对象,但Window浏览器将这个全局对象作为window对象的部分实现。
      • Math对象
        • 属性:Math.E等常量
        • min()和max()方法
          • 接收任意多个数值参数
          • 如果是数组,可借助apply()方法,Math.max.apply(Math, array),这样就可以找到数组的最大/小值
        • 舍入方法
          • ceil():向上舍入为最接近的整数
          • floor():向下舍入为最接近的整数
          • round():四舍五入
        • random() => [0, 1)
          • [lowerValue, upperValue)
          • 第一个可能的值:lowerValue
          • 可能值的总数:upperValue - lowerValue + 1
          • 可能值是连续
          • Math.floor(Math.random() * 可能值的总数 + 第一个可能的值
          • Math.floor(Math.random() * (upperValue - lowerValue + 1) + lowerValue)
        • 其他方法:Math.abs()等
  • 引用类型小结

    • 对象在JavaScript中被称为引用类型的值
    • 引用类型与传统OOP中的类相似,但实现不同。(JavaScript能直接访问引用类型/类吗?是用对象来模拟的类?只是名称第一个大写?)
    • Object是一个基础类型,其他所有引用类型都从Object继承了基本的行为。(但使用Object.create(null)创建的对象未继承!是一个空白的对象。这里指的是对象,继承发生在类之间)
    • 基本类型值可以被作为对象来访问(Boolean、Number、String)
    • 每个包装类型都映射到同名的基本类型
    • 在读取模式下访问基本类型值,就会创建对应的基本包装类型的一个对象,从而方便了数据操作。
    • 操作基本类型值的语句一经执行完毕,就会立即销毁新创建的包装对象。
    • 在所有代码执行之前,作用域中就已经存在两个内置对象:Global和Math。(其他内置对象不存在?)

母亲节H5活动页实现总结


距离活动页正式上线还有2天,心情比较复杂,需要写点东西沉淀下来。

首先交待一下背景

大概十几天前,产品妹子和运营妹子把我拉到了一个小黑屋,一脸坏笑地对我说,“母亲节快要到了,我们有一个活动页需要你支持一下”。我心想,“随便找个{易企秀}啊、{云凤蝶}啥的搭一下不就完了嘛,还要我来写一个?”。本着{能推就推}的原则,我也就暂且坐下来听听她们的需求细节。运营妹子说,“来,我给你看一个H5页面示例”,反手就拿出来了一个网易的活动链接,深夜,男同事问我睡了吗……,“我们就想做成这样子的一个效果,网上的那些模板我们都看过了,都做不了,所以就只要请大神帮帮忙”。哟呵,这首先拿了个大厂的案例给我来了个下马威,马上又大神大神的叫着,你说这我要是告诉人家“我不会”是多尴尬呀。“这很简单,不就一个H5吗,最多两个小时就给你搞定,不过你们得先把效果、图片啥的都准备好”

过了两天

需求大概确定了,运营妹子扔过来一个DEMO链接,“差不多就是这个样子了”。

布局

首先很明确这是一个独立的分页H5,每一页上会有一些动画或者视频,然后加上一个加载页,大概也就这些东西。

<!DOCTYPE html>
<html>
    <head>
    </head>
    <body id="wrapper"> 
        <section id="container">
            <section id="main-page-0" class="main-page loading current-page" data-index='0'></section>
            <section id="main-page-1" class="main-page" data-index='1'></section>
            <section id="main-page-2" class="main-page" data-index='2'></section>
            <section id="main-page-3" class="main-page" data-index='3'></section>
            <section id="main-page-4" class="main-page" data-index='4'></section>
            <section id="main-page-5" class="main-page" data-index='5'></section>
        </section>
    </body>
</html>
  • 总共就6个页面,分别是P0-加载页,P1-5
  • 每一个页面都平铺整个屏幕
  • 默认显示加载页display:block,其他所有页面都display:none
  • .current-page表示当前显示页面

如何保证每一页都平铺整个屏幕?

方案一:最开始想到的方案

.main-page{
    width: 100vw;
    height: 100vh;
}

感觉这样挺好,但是因为vwvh对于部分机型尚不兼容,所以不轻易采用这种写法。

方案二:后来看了一下{易企秀}的实现方案

body{
    width: 100%;
    min-height: 100%;
    position: fixed;
}
.main-page{
    width: 100%;
    height: 100%;
}

实现翻页

用户在滑动时,需要隐藏当前页面,显示上/下一页。借鉴项目以前的代码:需要在页面初始化后调用initTouch()给body绑定touch相关事件

    var $body = document.getElementsByTagName('body')[0];   // body
    // 页面触摸事件
    var touchStartListener = function (event) {
        var touches = event.targetTouches;
        if (touches.length == 1) {
            this.x = touches[0].clientX;
            this.y = touches[0].clientY;
        }
    }
    var touchMoveListener = function (event) {
        if(!isAllowTouch) return false;
        var touches = event.targetTouches;
        if (touches.length == 1) { //一个手指在屏幕上
            var x1 = touches[0].clientX, //移动到的坐标
                y1 = touches[0].clientY;
            var deltaY = y1 -this.y;
            var deltaX = x1 -this.x;
            if ((Math.abs(deltaY) < Math.abs(deltaX)) && (deltaX < 0) &&((x1 + 20) < this.x)){
               // 左滑
            }
            if ((Math.abs(deltaY) < Math.abs(deltaX)) && (deltaX > 0) &&((x1 - 20) > this.x)){
                // 右滑
            }
            if ((Math.abs(deltaY) > Math.abs(deltaX)) && (deltaY < 0) &&((y1 + 20) < this.y)){
                // 上滑
            }
            if ((Math.abs(deltaY) > Math.abs(deltaX)) && (deltaY > 0) &&((y1 - 20) > this.y)){
                // 下滑
            }
        }
    }
    var touchEndListener = function (event) {}
    var touchStartListenerThrottle = _throttled(touchStartListener, 50);
    var touchMoveListenerThrottle  = _throttled(touchMoveListener,  50);
    var touchEndListenerThrottle   = _throttled(touchEndListener,   50);
    var initTouch = function () {
        $body.addEventListener("touchstart", touchStartListenerThrottle);
        $body.addEventListener("touchmove", touchMoveListenerThrottle);
        $body.addEventListener("touchend", touchEndListenerThrottle);
    }
    var removeTouch = function () {
        $body.removeEventListener("touchstart", touchStartListenerThrottle);
        $body.removeEventListener("touchmove", touchMoveListenerThrottle);
        $body.removeEventListener("touchend", this.touchEndListenerThrottle);
    }

我们是用touchstarttouchmove/touchend来模拟滑动事件,理论上touchmove会持续(节流函数下会隔一段时间)触发,因此在实际情况下一次滑动仅且只能翻一页。节流函数如下:

// 节流函数
function _throttled(func, wait, options){
    // throttled节流
    var timeout, context, args, result
    var previous = 0
    if (!options) options = {}

    var later = function() {
        previous = options.leading === false ? 0 : Date.now()
        timeout = null
        result = func.apply(context, args)
        if (!timeout) context = args = null
    }

    var throttled = function() {
        var now = Date.now()
        if (!previous && options.leading === false) previous = now
        var remaining = wait - (now - previous)
        context = this
        args = arguments
        if (remaining <= 0 || remaining > wait) {
            if (timeout) {
                clearTimeout(timeout)
                timeout = null
            }
            previous = now
            result = func.apply(context, args)
            if (!timeout) context = args = null
        } else if (!timeout && options.trailing !== false) {
            timeout = setTimeout(later, remaining)
        }
        return result
    }

    throttled.cancel = function() {
        clearTimeout(timeout)
        previous = 0
        timeout = context = args = null
    }

    return throttled;
}

全屏自适应

每一页全屏实现了,但是如何实现每一屏内容自适应。
首先将屏幕分为三层:背景底层(纯色)、背景层、内容层(图片、文字、动画、视频等)。

  • 对于背景底层,只需设置背景色即可background-color
  • 对于背景层,分两种情况,有些需要平铺整个屏幕background-size:cover,有些则主要贴底background-position:bottom center,代码如下:
#main-page-3{
    background-image: url('../imgs/m/p1_2_3_shadow.png'), url('../imgs/m/p3_vision.png'), url('../imgs/m/p3_bg.png');
    background-size: cover, 100% auto, cover;
    background-repeat: no-repeat, no-repeat, no-repeat;
    background-position: center, bottom center, center;
}
  • 对于内容层,需要参照.main-page绝对定位显示即可

模拟加载进度

理论情况下我们是根据整个H5页面资源的加载情况来输出加载进度,但是实际情况下是很难准确的获取到这些信息的,因此大多数情况下的H5加载都是模拟的,这次的H5因为涉及到视频的播放,所以可以认为视频加载完成,则页面就加载完成,而在此之前都随机增长:

// 随机生成加载进度,模拟视频加载
function _randomProgress(initValue, step, duration, maxValue){
    var num = initValue;
    var tid = setTimeout(function () {
        num += parseInt(Math.random()*step);
        if(num >= maxValue){
            num = maxValue;
            clearTimeout(tid);
            tid = null;
            $('#loading-progress').text(num);
        }else{
            _randomProgress(num, step, duration,maxValue);
        }
        $('#loading-progress').text(num);
    }, duration);
}

页面切换和动画

首先我用了一个全局变量currentPageIndex来定位当前页面,当触发页面滑动时,首先隐藏当前页面,改变currentPageIndex的值,显示当前页面,显示当前页面的动画。

其次可以明确的是页面切换动画是逻辑上独立的两个过程,但是在实际情况中,两者是存在回调的关系的,例如页面切换后,就需要执行当前页面的动画,动画结束定格几秒后,就需要进行页面切换。其中前面还可以通过手动触发翻页来完成。

// 页面切换动效
var handlePageSwitch = function (step) {
    if((currentPageIndex <= 1 && step === -1) || (currentPageIndex === 5 && step === 1)) return false; // 限制页面滑动边界条件
    isAllowTouch = false;
    var $currentPage = $('#main-page-'+currentPageIndex);
    $currentPage.hide();
    currentPageIndex = currentPageIndex + step;
    pageAnimate(step);
}
// 页面动画
var pageAnimate = function (step) {
    var fadeInTime = 500;
    var $currentPage = $('#main-page-'+currentPageIndex);
    console.log('P' + (currentPageIndex-step) +'-P' + currentPageIndex);
    switch('' + currentPageIndex + step){
        // 0->1
        case '11':
            $currentPage.fadeIn(fadeInTime, function () {
                video.play();
                isAllowTouch = true;    // 测试
            });
            break;
        // 1->2
        case '21':
            video.currentTime = 0;
            video.pause();
            $currentPage.addClass('scale-page');
            $currentPage.fadeIn(fadeInTime, function () {
                console.log(1);
                setTimeout(function () {
                    $('#main-page-2 .words').fadeIn();
                }, 256)
                isAllowTouch = true;
            });
            break;
        // 2->3
        case '31':
            $('#main-page-2 .words').hide();
            $currentPage.removeClass('scale-page');
            $currentPage.fadeIn(fadeInTime, function () {
                $('#main-page-3 .words').fadeIn();
                isAllowTouch = true;
            });
            break;
        // 3->4
        case '41':
            $('#main-page-3 .words').hide();
            $currentPage.fadeIn(fadeInTime, function () {
                isAllowTouch = true;
            });
            break;
        // 4->5
        case '51':
            $currentPage.fadeIn(fadeInTime, function () {
                isAllowTouch = true;
            });
            break;
        // 5->4
        case '4-1':
            $currentPage.show();
            setTimeout(function () {
                isAllowTouch = true;
            }, fadeInTime);
            break;
        // 4->3
        case '3-1':
            $currentPage.show();
            setTimeout(function () {
                $('#main-page-3 .words').fadeIn();
                isAllowTouch = true;
            }, fadeInTime);
            break;
        // 3->2
        case '2-1':
            $('#main-page-3 .words').hide();
            $currentPage.removeClass('scale-page');
            $currentPage.show();
            setTimeout(function () {
                setTimeout(function () {
                    $('#main-page-2 .words').fadeIn();
                }, 256)
                isAllowTouch = true;
            }, fadeInTime);
            break;
        // 2->1
        case '1-1':
            $('#main-page-2 .words').hide();
            $currentPage.show();
            video.play();
            setTimeout(function () {
                isAllowTouch = true;
            }, fadeInTime);
            break;
        default:
    }
}

这里在页面动画中,我将各种具体情况都分条进行了处理,即“P0-P1,P1-P2,P2-P3,……,P5-P4,……”。所以当进入某一页时,需要把前一页的动画隐藏。即页面之前的动画处理逻辑耦合了。所以当需求发生变化的时候,例如页面动画时间变一变、渐入时间变一变,就很难维护,逻辑很混乱。

梳理后,代码如下:

// 页面切换
// @params step 1-前进;0-后退
var _pageChange = function(step){
    if((currentPageIndex <= 1 && step === 0) || (currentPageIndex === 5 && step === 1)) return false; // 限制页面滑动边界条件
    isAllowTouch = false;                                           // Step0:页面滑动状态设为false
    // var pageFlag = '' + currentPageIndex + step;
    $currentPage().hide();                                          // Step1:上一个页面直接消失
    _initPageAnimation();                                           // Step2:初始化当前页面内容及动画
    currentPageIndex += step ? 1 : -1;                              // Step3:页面索引改变
    $body.style.backgroundColor = bgColorArr[currentPageIndex];     // Step4:修改页面背景色

    $currentPage().fadeIn(pageSwitchFadeInTime, function () {       // Step5:页面渐入或+缩放动画
        if(currentPageIndex === 1 && step) audio.play();
        _pageAnimation();                                           // Step6:页面动画
        isAllowTouch = true;                                        // Step7:页面滑动状态设为true
    });
}

只需管理当前页面的动画效果,增加一个页面动画初始化的逻辑:

// 初始化页面动画/页面回复
var _initPageAnimation = function () {
    $('.init').hide();
    video.currentTime(0);
    video.pause();
}
// 页面动画
var _pageAnimation = function () {
    if(currentPageIndex != 0 && currentPageIndex != 5) $('#arrow').show();  // 左滑箭头显示
    switch(currentPageIndex){
        case 0:
            break;
        case 1:
            $('#arrow').show();
            video.play();
            break;
        case 2:
            $('#main-page-2 .init').fadeIn(wordFadeInTime, function () {
                if(currentPageIndex != 2) return false;
                setTimeout(function () {
                    if(currentPageIndex != 2) return false;
                    _pageChange(1);
                }, wordInAndPageChangeTime);
            });
            break;
        case 3:
            $('#main-page-3 .init').fadeIn(wordFadeInTime, function () {
                if(currentPageIndex != 3) return false;
                setTimeout(function () {
                    if(currentPageIndex != 3) return false;
                    _pageChange(1);
                }, wordInAndPageChangeTime);
            });
            break;
        case 4:
            $('#main-page-4 .init').fadeIn(wordFadeInTime, function () {
                if(currentPageIndex != 4) return false;
                setTimeout(function () {
                    if(currentPageIndex != 4) return false;
                    _pageChange(1);
                }, wordInAndPageChangeTime);
            });
            break;
        case 5:
            $('#arrow-up').show();
            break;
        default:
    }
}

编码前没能做好设计!后期有时间考虑借助面向对象**实现

视频处理

因为之前从未接触过H5下视频播放的问题,但是一经手才发现处理好视频真不是一件容易的事情。

方案一:使用原始video标签
在分析上面提到的那个网易的H5页面时,发现他用的也是原生的video标签(现在想来,如果利用了什么库,最后呈现在浏览器的也可能是video标签),只不过我发现其中有这么一段代码(混淆后):

var c = new XMLHttpRequest;
    c.open("GET", r, !0),
    c.responseType = "blob",
    c.onload = function() {
        if (200 === this.status && "video/mp4" === this.response.type) {
            var i = this.response
                , a = (window.URL || window.webkitURL || window || {}).createObjectURL(i);
            n(s),
            e(l),
            o.src = a
        } else
            t()
    }
    ,
    c.onerror = function(e) {
        console.log(e),
        t()
    }
    ,
    c.send()

发现用到了一个blob对象,虽然不明觉厉,自己也手把手的用上了:

// 视频初始化
var _initVideo = function (cb) {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', videoPath, true);
    xhr.responseType = 'blob';
    xhr.onload = function() {
        if (200 === this.status && "video/mp4" === this.response.type){
            var res = this.response;
            var url = (window.URL || window.webkitURL || window || {}).createObjectURL(res);
            video.src = url;
        }else{
            video.src = videoPath;
            videoLoaded = true;
            cb && cb();
        }
    }
    xhr.onerror = function(e) {
        console.log(e);
        video.src = videoPath;
        videoLoaded = true;
        cb && cb();
    }
    xhr.send();
    video.addEventListener('loadstart', function () {
        videoLoaded = true;
        handlePageSwitch(1);
        cb && cb();
        console.log('video start load.');
    });
    video.addEventListener('ended', function () {
        // 视频播放结束
        console.log('video is over!');
        handlePageSwitch(1);
    });
}

本来视频播放也没啥问题,但老是提心吊胆,感觉早晚出bug。

方案二:借助video.js库
后来就换了种实现方式——借助video.js,重新写了下视频的逻辑:

var _initVideo = function () {
    video.ready(function () {
        videojs.log('Your player is ready!');

        this.on('loadedmetadata', function () { // 因为iOS下video无法自动加载,因此没办法触发canplaythrough-当浏览器预计能够在不停下来进行缓冲的情况下持续播放指定的音频/视频时,会发生 canplaythrough 事件。
            if(isVideoLoaded) return false;
            setTimeout(function () {
                console.log('loading@ '+100+'%');
                $loadingProgress.text(100);  // 进度置为100
                isVideoLoaded = true;
                setTimeout(function () {
                    _pageChange(1);
                }, 1000);   // 加载完成1s后进入下一页
            }, 2000);
        });

        this.on('ended', function() {
            videojs.log('Awww...over so soon?!');
            setTimeout(function () {
                _pageChange(1);
            }, 2000);   // 视频播放完成2s后进入下一页
        });
    });
}

发现两个问题:

  • video在iOS下无法自动加载(暂未解决,将触发条件移至loadedmetadata)
    理论上,当音频/视频处于加载过程中时,会依次发生以下事件:loadstart、durationchange、loadedmetadata、loadeddata、progress、canplay、canplaythrough

  • 部分机型(如红米、部分iPhone6)播放黑屏
    发现是分辨率过高的问题,调整后可以播放。

视频定位

还有一个棘手的问题是,我们的视频是需要在一个容器内播放的,如何将video定位到容器内需要考虑。

首先和设计师沟通,我们制作的视频尺寸是1:2(mix2录制),那给我的容器框也应该是1:2的。

其次因为要满足各手机尺寸自适应的问题,我需要将容器作为背景贴底居中显示,那此时我只需要将video外包裹一个div相对屏幕绝对居中。

如何保证div的比例,借助padding相对于父容器width实现。

.video{
    width: 67%; // 父容器width的67%
    height: 0;
    padding-bottom: 134%; // 父容器width的134%
}

小结

总体下来,本以为2个小时就能搞定的,最终因为各方面因素做了持续10天左右(当然主要原因还是需求不断变更的问题,哈哈),但是这个“小”的H5,最终还是没能做到尽善尽美,优化之路还很长。

《21天减肥实战营》课程复盘

从3.11到4.1这21天里,我参加“在行一点(原分答)”里的一个“21天减肥实战营”课程,成功减重4kg,很多朋友咨询课程内容,就想着总结下来分享给大家。

其实之前我是认为自己和减肥根本挨不着边的,但是17年底公司员工体检查出自己居然已经是“三高”(高血压、高血脂、高尿酸)人群!去医院复诊时,医生明确说我这种属于由于快速增重引起的“代谢综合征”,再不控制这些指标就需要用药治疗。但是一旦用药以后就很难离开了,毕竟我还这么年轻啊。所以痛定思痛,毅然决定减肥。

成果

image

课程内容

课程内容分为两块:

  • 减肥技能
  • 任务
    • 今日食谱
     今日食谱就是你每天需要吃什么、吃多少。
    
     首先,给了食谱,意思就是说,大家在课程这段时间,吃什么、吃多少,就必须按照食谱来了。**食谱以内的东西,才可以吃**,但也必须按照食谱要求的**量**来吃。**食谱以外的东西什么都不能吃**。这样才能达到最好的效果。
    
     食谱分成两套(一套自己做饭的食谱、一套无法自己做饭的食谱),每一套又按照性别、体重来区分出很多种,你是什么性别,什么体重区间,就选择相应的食谱。
    
     最后,不管是用哪一套食谱,都要给食物称重,按照要求的量吃东西,这样才能获得最好的效果。我们提前要求大家买食品秤就是这个目的。
    
     我的食谱里,只给出食材和每一种食材的分量,不会过多限制加工方法,给你充分的灵活性。但是减肥饮食,肯定不可能大油炒、或者油炸。建议清炒、水煮、蒸、烤、微波炉加工。
    
     为了方便大家两套食谱以**天**为单位轮换使用,我尽可能的把每天的食谱都设计的差不多,不会有太多变化,这样你可以今天用这套,明天用另一套。但是在一天之内,不能混用两套食谱。
    
    
    • 饮食技巧
     1、每口食物缓慢咀嚼39次再咽下,完全咽下上一口食物之后,再吃下一口;
     2、咀嚼食物的时候放下所有餐具和手里的食物,只要嘴里有食物了,就清空两只手;
     3、吃东西的时候集中注意力在咀嚼上,不要看电视、看视频、看书或者听音频节目;
     4、小口吃饭,原来的一口分成2-3口;
     5、每顿饭如果感觉吃饱了食物还没吃完,也不要再吃了。
    
    • 运动训练
     布置的运动任务不需要去健身房,大概分为户外运动和家用运动,其中家用运动方式很多,我们还专门制作了GIF动图。而且各种运动方式之间很多都可以互换,也就是说,你完全可以足不出户完成运动任务。
    
     每天运动的时间不长,但如果实在没有整块的时间,运动任务**也可以分开完成**。比如要求每天原地踏步30分钟,没有整块时间,也可以分成2个15分钟来完成。
    
    • NEXT训练(非运动性产热)
     NEAT就是平时日常活动的热量消耗。
    

课前准备清单

dcd5ba8aab7f2c082190541194d9_943x606
7ab7643d10f819900ffc6483ad32_942x278

小贴士:在开始前记录下你的身高、体重等信息,拍摄一张减肥前的全身照。

重要说明

  • 关于饮食的重要说明
1、饮食中严格遵守食谱最底端的饮食技巧;
2、每天的食谱尽量设计的非常类似,所以两套食谱可以以“天”为单位替换使用;
3、食谱中所有食物,除了“糙米饭”之外都是生的重量;
4、所有食物的重量都是指可食用部分的重量;
5、不可以使用沙拉酱、肉酱以或任何有油的酱料;
6、每天总共吃的东西不超过食谱要求就可以,每天食谱当中各餐之间可以替换;
7、植物油的使用量必须符合食谱的要求,一小勺是指一小咖啡勺(约5g);
8、在油的使用上,如果有橄榄油和亚麻籽油更好,没有的话也可以换成其它植物油;
9、进餐时间没有严格要求;
10、食材加工方式不做具体要求(植物油有限制,炸是不可能的),建议水煮、蒸、无油烤、清炒、煲汤等;
11、盐、酱油等调味料没有严格限制,但不建议使用太多;
12、上午加餐时间为上午两餐之间居中的时间点,下午加餐时间则是下午两餐之间的居中时间点;
13、任何蜂蜜、白糖、红糖、黑糖等都不可以吃;
14、注意区别“鸡蛋”和“蛋清”。鸡蛋指全蛋,蛋清只是鸡蛋清,不包括蛋黄;
15、肉类的选择本身是低脂肪,但买的时候也要二次把关,保证所有肉类都是瘦肉,有肥肉的不可以吃;
16、喝水不限制,各种不添加奶、糖的自己泡的茶都可以喝,咖啡只可以和黑咖啡。但因为减肥时代谢产水会减少,所以适当多喝点水也没坏处;
17、“血制品”所有动物血都可以,如鸭血、猪血等。当然,加工方式要自己加工或者符合我们对植物油的要求;
18、血制品、纯瘦牛肉建议大家要每周吃1-2次,对补铁很有好处;
19、胡椒粉、辣椒粉可以少量用,不能太多;
20、酸奶要无其它食物添加的原味酸奶,酸奶一般都有一点糖,不是很多就可以;
21、牛奶是纯牛奶,脱脂全脂都可以。牛奶和酸奶可以等量替换。
  • 关于运动的重要说明
1、经期不可以做跑、跳动作的运动,用原地踏步代替即可,其它运动、NEAT可以酌情完成;
2、运动和NEAT都可以分开完成,一天加在一起总时间达到要求即可。但一次性完成更好,因为分开完成更容易给自己“通融”;
3、NEAT可以充分利用自己的生活、工作中的碎片时间。比如步行上下班、提前几站下车、站着看电视、站着看书、站着完成某些工作等等;
4、每日NEAT任务不可替换;
5、自己平时已有的运动可以用来代替作业里的运动,多运动没关系,不要少于作业的强度和时间要求即可;
6、瑜伽的种类很多,热量消耗不好衡量,所以不能用瑜伽替换任何运动;
7、游泳要看游动的时间,如果多数时间是浮水,那么不算是有效的运动。
  • 备选食物说明
    default

H5 过渡页Loading效果总结

随着 H5 的普及,各种 CSS 3 动画及 JS 特效在网页中层出不穷,PC端载入速度还可以,移动端则相对要慢很多,如果图片或脚本没有完全载入,用户在操作中可能会发生各种问题。因此我们需要对数据载入进行侦测,以更加人性化的方式给用户展现。

以下总结实现的一些现成好看的资源和涉及的相关逻辑:

效果资源

  • 很不错的在线loading效果网站资源,loading.io(可生成gif、svg以及CSS 3 代码

相关逻辑处理

  • 加载状态事件:document.onreadystatechange - 页面加载状态改变时的事件
  • 获取当前文档的加载状态:document.readyState
    • uninitialized - 还未开始载入
    • loading - 载入中
    • interactIve - 已加载,文档与用户可以开始交互
    • complete - 载入完成

参考

记一次有意思的Debug

有趣的是,遇到的 Bug 并不是代码层面的,仔细想想还蛮有意思的。

TL;DR

我发现最近我在使用 VSCode 写样式的过程中,每次从小程序 IDE 里复制 {类名} 到编辑器时,总是会弹出终端,如下图所示:

tim 20180730152206

第一反应是我不小心设置了快捷键,当按下 Ctrl+V 时会弹出终端,于是便打开 VSCode 快捷键的设置项,“不出所料”,果然被我找到了:

tim 20180730152146

果断,“右键——删除键绑定”,重启,应该没问题了吧。

但是,还是出现上面的情况,这可急死我了,为啥改了快捷键还不生效呢???

后来,同事一看:

你这哪是 {快捷键} 的问题,明显是报错啦,你仔细看终端里提示的,有个 Error ... 你是不是装了啥插件?

我一想,好像真的安装了格式化代码的插件,但是怎么会这样呢?

而且找了安装的插件,也没有相关的设置呀,没办法,想想最近 VSCode 有次更新,之前的 IDE 配置都是通过 setting.json 文件来配置的。这次更新提供了 {可视化配置} 的方法。

tim 20180730152623

当时我好想改了好多配置,没办法一个个找吧,果然找到了“罪魁祸首”:

tim 20180730152721

原因就是,当我粘贴内容时,就会触发“格式化程序”自动格式化代码,但是这时候的语法肯定是不对的,所以就会报错。


为什么要记录下来呢?因为我发现自己好像不怎么会看报错信息,很多时候会忽略掉报错信息,记录下来就是强化自己这方面的意识。

--EOF--

记一次有道云笔记微信无法登录bug解决方案

前几天通过手机把电脑端的有道云笔记下线了,发现在电脑上没办法登录了,折腾了好几天,最终咨询了人工客服解决了(赞网易的用诉)。记录如下:

1.更换默认浏览器后,去登录页面点击微信按钮授权登录看看;

2.清除当前默认浏览器的缓存后,再去授权试试;

3.您确认网页版的笔记都正常后,完全云笔记退出软件(在系统托盘点击笔记图标右键选择退出),在>计算机本地搜“ynote”文件夹,待其搜索完毕,将“ynote”里的文件清空,再重启软件登录账户试试

后来问为啥不支持微信账号绑定邮箱登录,给出解释及解决方法:

我能不能微信登录后绑定个邮箱啥——这个问题 目前没有绑定功能呢 各个账户都是独立的 如果您怕授权登录账号有问题 可以用网页版的导入笔记功能 把微信里的数据迁移到新的账号里

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.