三、Web前端开发JavaScript高薪课堂

郁子大约 62 分钟约 18534 字笔记渡一教育语言基础姬成JavaScript

(一)JavaScript 入门

1.Web 发展史

  • Mosaic,是互联网历史上第一个获普遍使用和能够显示图片的 网页浏览器 ,于 1993 年问世

2.浏览器组成

  • shell 部分
  • 内核部分
    • 渲染引擎(语法规则和渲染)
    • JS 引擎
    • 其他模块

3.翻译过程

1)编译

  • 通篇翻译
  • 将格式 1 的文件翻译成格式 2 的文件,由系统程序执行格式 2 文件
  • 该类型的编程语言叫编译性语言
  • 优点:过程快
  • 缺点:翻译后的格式 2 文件移植性不好(不能跨平台)
  • 如:
    • C 语言: .c => .obj
    • C++

2)解释

  • 逐行翻译,逐行执行
  • 直接翻译格式 1 的文件,翻译一行就交给系统程序执行一行
  • 该类型的编程语言叫解释性语言
  • 优点:直接翻译成机器码,可以跨平台
  • 缺点:过程稍微慢一点
  • 如:
    • JavaScript
    • PHP
    • Python

3)Java

  • Java 既不是编译性语言,也不是解释性语言,是 oak 语言
  • .java 文件通过 javac 命令编译成 .class 文件,再经过 jvm 虚拟机解释执行
  • 可以跨平台

4.JS 特点

  • 解释性语言
    • 不需要编译成文件,可以跨平台
  • 单线程
  • ECMA 标注
    • 兼容于 ECMA 标准,因此也称为 ECMAScript
  • 三大部分:
    • ECMAScript(ES)
    • DOM
    • BOM

5.主流浏览器

浏览器内核
IETrident -> Edge
FirefoxGecko
ChromeWebkit -> Blink
SafariWebkit
OperaPresto -> Blink

6.JS 引入

  • 页面内嵌标签
    • <script type="text/javascript"></script>
  • 外部引入
    • <script type="text/javascript" src="location"></script>
  • 为符合 Web 标准(W3C 标准中的一项),结构(HTML)、样式(CSS)、行为(JS)相分离
    • 通常采用外部引入的方式

7.JS 基本语法

1)变量 variable

  • 变量声明
    • 声明、赋值分解
    • 单一 var 模式(多个变量一次性声明并赋值)
  • 命名规则
    • 变量名必须以 英文字母_$ 开头
    • 变量名可以包括 英文字母_$数字
    • 不可以用系统的关键字、保留字作为变量名
// 声明
var a;
// 赋值
a = 100;

// 也可以同时进行
var b = 200;

// 单一var
var c = 300,
  d = 400,
  e = 500;
  • 关键字
1234567
breakelsenewvarcasefinallyreturn
voidcatchforswitchwhiledefaultif
throwdeleteintrydoinstanceoftypeof
  • 保留字
1234
abstractenumintshort
booleanexportinterfacestatic
byteextendslongsuper
charfinalnativesynchronized
classfloatpackagethrows
constgotoprivatetransient
debuggerimplementsprotectedvolatile
doubleimportpublic

2)值类型——数据类型

  • 不可改变的原始值(栈数据)
    • 拷贝的是值
    • Number
    • String
    • Boolean
    • undefined
    • null
  • 引用值(堆数据)
    • 拷贝的是地址
    • Array
    • Object
    • Function
    • Date
    • RegExp
  • 栈(stack)
    • FILO,先进后出,有底没顶
  • 堆(heap)

相关信息

原始值不可改变:

变量重新赋值实际上是在栈顶再申请一块空间,将变量地址指向新空间,原地址重置为原始地址

8.JS 语句基本规则

  • 语句后面要用分号 ; 结束
  • JS 错误
    • 低级错误(语法解析错误)
      • 逐行翻译前会通篇扫描,如果有解析错误,程序直接终止
    • 逻辑错误(标准错误)
      • 逐行翻译,遇到逻辑错误就停止在当前代码行
    • 语法错误会引发后续代码终止,但不会影响其它 JS 代码块
  • 书写格式要规范, =+/- 左右两边都应该有空格

9.运算操作符

  • +
    • 数学运算
    • 连接字符串
    • 任何数据类型加字符串都等于字符串
  • -*/%=()
    • 优先级 = 最低, () 最高
  • ++--+=-=*=/=%=
document.write(1 / 0); // Infinity(Number)
document.write(0 / 0); // NaN(Number) => Not a Number
  • ,运算符
    • 格式: (x, y)
    • 先计算表达式 x 的结果,再计算表达式 y 的结果,最后将表达式 y 的结果返回
var a = (1 - 1, 1 + 1);
console.log(a); // 2

var f = (function f() {
  return "1";
},
function g() {
  return 2;
})();
console.log(typeof f); // number

10.比较运算符

  • ><==>=<=!=
  • 比较结果为 boolean 值
  • 字符串比较的是 ASCII 码
    • 0:48
    • A:65
    • a:97
  • Infinity === Infinity
  • NaN !== NaN
  • {} == {} => false
    • 数组和对象比较的是地址值,空对象指向两个不同的地址

11.逻辑运算符

  • &&
    • 全真为真
    • 先看符号前的表达式 1,判断其结果转换为 boolean 值是否为真
        • 只有两个表达式,那么返回符号后的表达式 2 的结果
        • 不止两个表达式,继续判断下一个表达式转换为 boolean 值是否为真,重复上述逻辑
        • 返回符号前的表达式 1 的结果(不一定返回 false)
    • 利用该特性可以作 短路语句 使用
      • 2 > 1 && document.write("我输出了");
  • ||
    • 全假为假
    • 先看符号前的表达式 1,判断其结果转换为 boolean 值是否为真
        • 返回符号前的表达式 1 的结果
        • 只有两个表达式,那么返回符号后的表达式 2 的结果
        • 不止两个表达式,继续判断下一个表达式转换为 boolean 值是否为真,重复上述逻辑
    • 利用该特性可以作 或者 使用
      • var event = e || window.event;
  • !
    • 非真即假
    • 先将表达式转换为 boolean 值,再取反,返回结果
  • 运算结果为真实的值

相关信息

转换为 boolean 值后为 false 的表达式:

  • undefined
  • null
  • NaN
  • ""
  • 0
  • false

12.条件语句

  • if () {}
  • if () {} else if () {}
  • if (a && b) {}
    • a 成立 并且 b 成立时才执行
  • if (a || b){}
    • a 成立 或者 b 成立时执行

13.循环语句

  • for (var i = 0; i < len; i++) {}
  • while(i < len) {}
  • do {} while (i < len)
    • 无论如何都先执行一次

14.类型判断

  • typeof() 或者 typeof 值
  • 返回以下类型:
    • number
    • string
    • boolean
    • object
    • undefined
    • function
  • 原始值问题:表示泛泛的引用值返回 object
  • 历史遗留问题:表示空的占位符返回 object

警告

  • typeof({}) = object
    • 原始值问题
  • typeof([]) = object
    • 原始值问题
  • typeof(null) = object
    • 历史遗留问题
  • 在 JS 中使用未定义的值会报未定义错误
    • console.log(a); => error: not defined
    • 当且仅当 typeof 这一种情况可以使用未定义的值且不报错
      • console.log(typeof(a)) => "undefined"
      • typeof(typeof(a)) => string
/**
 * if的括号会把内部的f函数声明转变为表达式,此时f变为立即执行函数
 * f的函数名无效,即 f 未声明
 * 但是typeof后面加未声明的变量不报错,会返回undefined 【1 + undefined = NaN】
 * 且typeof返回的是字符串类型,所以最终结果为 1undefined 的字符串
 */
var x = 1;
if (function f() {}) {
  x += typeof f;
}
console.log(x); // 1undefined

15.类型转换

1)显式类型转换

  • Number(mix)
    • 关注点在于转成数字类型
    • boolean 值会转换
Number(true); // 1
Number(false); // 0
Number(null); // 0
Number(undefined); // NaN
Number("a"); // NaN
Number("-123"); // -123
Number("123abc"); // NaN
  • parseInt(string, radix)
    • 关注点在于转成整型
      • boolean 值不会转换
      • 从数字位开始转换至最后一位数字位,遇到非数字位则截断
    • 直接舍去小数点后的数字
    • radix 是基底,表示转换时采用的进制
      • 以 radix 为基底转成十进制的整数
      • 如:2、8、10、16
      • 如果限制了进制,但是转的数字/字符串不是“1010”组合,则返回 NaN
    • 如果 radix 是 0
      • 有些浏览器会忽略 radix,当成 parseInt(string)
      • 有些浏览器会报错,或者返回 NaN
parseInt(true); // NaN
parseInt(false); // NaN
parseInt(null); // NaN
parseInt(undefined); // NaN
parseInt("123"); // 123
parseInt(123.3); // 123
parseInt("123.9"); // 123
parseInt("123abc"); // 123
parseInt("3", 2); // NaN
parseInt("10101010", 2); // 170
  • parseFloat(string)
    • 关注点在于转成浮点型
      • 从数字位开始转换至最后一位数字位,遇到除第一个小数点外的非数字位则截断
parseFloat("123"); // 123.0
parseFloat(123.3); // 123.3
parseFloat("12.3abc"); // 12.3
  • toString(radix)
    • 关注点在于转成字符串
    • 用法不同于其他,需要使用调用函数的形式
    • radix 是基底,表示转换时的目标进制
      • 以十进制为基底转成 radix 进制的字符串
      • 如:2、8、10、16
let a = 123;
a.toString(); // "123"

let b = true;
b.toString(); // "true"

let c = 10;
c.toString(8); // "12" => 1 * 8 ^ 1 + 0 * 8 ^ 1 = 10

警告

undefined 和 null 不能调用 toString()

会直接报错

  • String(mix)
    • 关注点在于转成字符串
String(true); // "true"
String(false); // "false"
String(null); // "null"
String(undefined); // "undefined"
String(123); // "123"
  • Boolean()
    • 关注点在于转成布尔值
    • 除了以下六个值,其余全为 true
      • undefined
      • null
      • NaN
      • ""
      • 0
      • false
Boolean(true); // true
Boolean(false); // false
Boolean(null); // false
Boolean(undefined); // false
Boolean(123); // true

2)隐式类型转换

  • isNaN()
    • 先把参数使用 Number() 转换,再将结果和 NaN 作比较
isNaN(NaN); // true
isNaN("NaN"); // false
isNaN(123); // false
isNaN("123"); // false
isNaN("abc"); // true
isNaN(null); // false
isNaN(undefined); // true
function myIsNaN(num) {
  var ret = Number(num);
  ret += "";
  if (ret == "NaN") return true;
  return false;
}
  • ++--
    • 自增自减
    • 先使用 Number() 转换,再将结果自增/自减
  • +-
    • 一元正负
    • 先使用 Number() 转换,再将结果转成正/负
let a = "123";
a++; // 124

let b = "abc";
+b; // NaN
  • + 加法
    • 只要符号左右两个表达式其中之一是字符串,就会调用 String() 转成字符串类型
  • -*/%
    • 先使用 Number() 转换,再运算结果
let b = "a" * 1;
=> Number("a") * Number(1)
=> NaN * 1
=> NaN
  • &&||!
    • 表达式转成 boolean 值用于判断,返回的是表达式的值
  • <><=>=
    • 只要符号左右两个表达式其中之一是数字,就会调用 Number() 转成数字类型再比较
    • 如果没有数字类型则转成字符串类型比较
    • 两个字符串类型
      • 比较的是 ASCII 码大小
      • 逐位比较
  • ==!=
    • 判断是否相等,返回 boolean 值
false > true => false
2 > 1 > 3 => false
2 > 3 < 1 => true
10 > 100 > 0 => false
100 > 10 > 0 => true
undefined > 0 => false
undefined < 0 => false
undefined == 0 => false
null > 0 => false
null < 0 => false
null == 0 => false
undefined == null => true
undefined === null => false
NaN == NaN => false

3)不发生类型转换

  • ===!==
    • 绝对等于、绝对不等于
    • 判断值,也判断类型
1 === 1 => true
1 === "1" => false
1 !== 1 => false
1 !== "1" => true
NaN === "NaN" => false
NaN !== "NaN" => true

(二)函数

1.定义

  • 和数组、对象一致,也是引用值类型
  • 保存在堆内存中,栈中保存堆内地址

1)函数声明

function fn() {
  // code here
}

2)命名函数表达式

var fn = function fn2() {
  // code here
};
console.log(fn); // function fn2 () {}
console.log(fn2); // not defined
console.log(fn.name); // "fn2"

3)匿名函数表达式

  • 该方式更常用,所以简称为 函数表达式
var fn = function () {
  // code here
};
console.log(fn); // function () {}
console.log(fn.name); // "fn"

2.组成形式

  • 函数名称
  • 参数
    • 形式参数——形参
      • 函数的 length 属性绑定的是形参的个数
      • 函数名.length
    • 实际参数——实参
      • 函数体内中自定义了 arguments 实参列表,存储所有实参
    • 形参实参个数不需要保持一致
  • 返回值

1)arguments 和形参

  • arguments 保存的是实参列表
  • arguments 和形参占用两块不同的内存
  • JS 内部定义了映射规则,只要 arguments 或形参中某一位置的值变化了,另一个位置相同的值也变化
function sum(a, b) {
  // arguments: [1, 2]
  // var a = 1;

  a = 2;
  console.log(arguments[0]); // 2

  arguments[0] = 3;
  console.log(a); // 3
}
sum(1, 2);
  • arguments 和形参的映射关系只在初始时确定,未传值的形参不和 arguments 建立映射
  • 一开始调用函数时 arguments 有多少个参数就有多少个
  • 当实参数量少于形参时,即使函数体内再重新给未传值的形参赋值,此时对应的 arguments 下标的值也不会变化,仍然是 undefined
function sum(a, b) {
  // arguments: [1]
  // var a = 1;

  b = 2;
  console.log(arguments[1]); // undefined
}
sum(1);

3.递归

  • 找规律
    • 符合人的思维模式
  • 找出口
    • 结束的时间点
  • 先执行的最后被返回

注意

递归唯一的好处就是 使代码变简洁

不能让程序执行变快,反而可能更慢

1)n 的阶乘

function mul(n) {
  if (n === 0 || n === 1) return 1;
  return n * mul(n - 1);
}

2)斐波那契数列

function fb(n) {
  if (n === 1 || n === 2) return 1;
  return fb(n - 1) + fb(n - 2);
}

(三)预编译

1.JS 运行三部曲

  • 语法分析
    • 通篇扫描代码,确定没有语法错误
  • 预编译
  • 解释执行
    • 解释一行,执行一行

2.预编译

  • 变量的声明提升,赋值不提升
    • 声明前使用变量,值为 undefined
  • 函数声明整体提升
    • 声明前调用函数,不报错,可执行

重要

预编译发生在函数执行的前一刻

1)前奏

  • imply global 暗示全局变量
    • 任何变量,如果变量 未经声明就赋值 ,此变量就为全局对象 window 所有
    • eg:
      • a = 123;
      • var a = b = 123; => window.b = 123;
  • 一切声明的 全局变量 ,全是 window 的属性
    • eg:
      • var a = 123; => window.a = 123;

2)四部曲

  • 函数体内的预编译
    • 创建 AO(Activation Object) 对象【执行期上下文/活动对象】
    • 找形参和变量声明,将变量和形参名作为 AO 属性名,值为 undefined【变量声明提升】
    • 将实参值和形参统一
    • 在函数体里面找函数声明,值赋予函数体
/*
AO {}
AO { a: undefined, b: undefined }
AO { a: 1, b: undefined }
AO { a: function a() {}, b: undefined, d: function d() {} }
*/
function fn(a) {
  console.log(a); // function a() {}
  var a = 123; // var声明提升,执行时略过声明,只修改AO中的a:function a() {} => 123
  console.log(a); // 123
  function a() {} // 函数声明整体提升,执行时略过此行
  console.log(a); // 123
  var b = function () {}; // var声明提升,执行时略过声明,只修改AO中的b:undefined => function () {}
  console.log(b); // function () {}
  function d() {}
}
fn(1);
/*
AO {}
AO { a: undefined, b: undefined, c: undefined }
AO { a: 1, b: undefined, c: undefined }
AO { a: 1, b: function b() {}, c: undefined, d: function d() {} }
*/
function test(a, b) {
  console.log(a); // 1
  c = 0; // AO { a: 1, b: function b() {}, c: 0, d: function d() {} }
  var c;
  a = 3; // AO { a: 3, b: function b() {}, c: 0, d: function d() {} }
  b = 2; // AO { a: 3, b: 2, c: 0, d: function d() {} }
  console.log(b); // 2
  function b() {}
  function d() {}
  console.log(b); // 2
}
test(1);
/*
AO {}
AO { a: undefined, b: undefined }
AO { a: 1, b: undefined }
AO { a: function a() {}, b: undefined }
*/
function test(a, b) {
  console.log(a); // function a() {}
  console.log(b); // undefined
  var b = 234; // AO { a: function a() {}, b: 234 }
  console.log(b); // 234
  a = 123; // AO { a: 123, b: 234 }
  console.log(a); // 123
  function a() {}
  var a;
  b = 234; // AO { a: 123, b: 234 }
  var b = function () {}; // AO { a: 123, b: function () {} }
  console.log(a); // 123
  console.log(b); // function () {}
}
test(1);
  • 全局内的预编译
    • 创建 GO(Global Object) 对象【执行期上下文/活动对象】
    • 找变量声明,将变量作为 GO 属性名,值为 undefined【变量声明提升】
    • 找函数声明,值赋予函数体

相关信息

window 对象就是 GO 对象

/*
GO {}
GO { a: undefined }
GO { a: function a() {} }
*/
var a = 123; // GO { a: 123 }
function a() {}
console.log(a); // 123
console.log(window.a); // 123
  • 函数内未声明就赋值的变量归 window 所有
/*
GO { b: 123 }
AO { a: undefined }
*/
function test() {
  var a = (b = 123);
  console.log(window.a); // undefined
  console.log(window.b); // 123
}
test();
  • 先生成 GO 再生成 AO
/*
GO {}
GO { test: undefined }
GO { test: function test() {} }
*/
console.log(test); // function test() {}
function test(test) {
  console.log(test); // function test() {}
  var test = 234; // AO { test: 234 }
  console.log(test); // 234
  function test() {}
}
/*
AO {}
AO { test: undefined }
AO { test: 1 }
AO { test: function test() {} }
*/
test(1); // GO { test: function test() {} } AO { test: 234 }
var test = 123; // GO { test: 123 } AO { test: 234 }
  • 函数体内和全局有同名变量时,先使用离得近的【就近原则】
/*
GO {}
GO { global: undefined, fn: function fn() {} }
GO { global: 100, fn: function fn() {} }
*/
global = 100;
function fn() {
  console.log(global); // undefined
  global = 200;
  console.log(global); // 200
  var global = 300; // AO { global: 300 }
}
/*
AO {}
AO { global: undefined }
AO { global: 200 }
*/
fn();
var global;
  • 预编译不看 if 等其他限定条件,只要有变量声明和函数声明,都提升
/*
GO {}
GO { a: undefined }
GO { a: undefined, test: function test() {} }
*/
function test() {
  console.log(b); // undefined
  if (a) {
    var b = 100;
  } // a是undefined,不走判断,AO { b: undefined }
  console.log(b); // undefined
  c = 234; // GO { a: undefined, test: function test() {}, c: 234 }
  console.log(c); // 234
}
var a;
/*
AO {}
AO { b: undefined }
*/
test();
a = 10; // GO { a: 10, test: function test() {}, c: 234 }
console.log(c); // 234
  • 百度 2013 年笔试题
/*
GO {}
GO { bar: function bar() {} }
*/
function bar() {
  return foo;
  foo = 10;
  function foo() {}
  var foo = 11;
}
/*
AO {}
AO { foo: undefined }
AO { foo: function foo() {} }
*/
console.log(bar()); // function foo() {}
/*
GO {}
GO { bar: function bar() {} }
*/
/*
AO {}
AO { foo: undefined }
AO { foo: function foo() {} }
AO { foo: 11 }
*/
console.log(bar()); // 11
function bar() {
  foo = 10;
  function foo() {}
  var foo = 11;
  return foo;
}

(四)作用域、作用域链

1.作用域 [[scope]]

  • 每个 JavaScript 函数都是一个对象,对象中有些属性可以访问,有些不可以,这些属性仅供 JavaScript 引擎存取
    • [[scope]] 就是其中一个
  • 存储了运行期上下文的集合

2.作用域链

  • [[scope]] 中所存储的执行期上下文对象的集合,呈链式连接

3.运行期上下文

  • 当函数执行的前一刻时,会创建一个称为 执行期上下文 的内部对象(AO,Activation Object)
  • 一个执行期上下文定义了一个函数执行时的环境
  • 函数每次执行时对应的执行上下文都是独一无二的
  • 所以多次调用一个函数会导致创建多个执行上下文,函数执行完毕时所产生的执行上下文会被销毁

4.查找变量

  • 在哪个函数内查找变量,就找那个函数的作用域链
  • 执行时的 作用域链的 顶端 依次向下查找

5.理解

function a() {
  function b() {
    var b = 234;
  }
  var a = 123;
  b();
  console.log(a);
}
var glob = 100;
a();
  • a 函数被定义: a.[[scope]] --> 0: GO {}

  • a 函数被执行: a.[[scope]] --> 0: AO {}, 1: GO {}

  • b 函数被定义: b.[[scope]] --> 0: AO {}, 1: GO {}
    • a 函数执行导致 b 函数被定义,所以 b 函数是基于 a 函数的环境被定义的

  • b 函数被执行: b.[[scope]] --> 0: AO {}, 1: AO {}, 2: GO {}
    • 地址 1 的 AO 保存的是 a 函数执行时产生的 AO 的引用

  • b 函数执行完成时,会删掉当前地址 0 对 AO 的引用(销毁 b 函数产生的执行上下文)
    • 此时 a 函数执行完成,删掉当前地址 0 对 AO 的引用(销毁 a 函数产生的执行上下文)
    • a 函数产生的执行上下文包括 b 函数的声明【b(function)】,此时 b 函数完全销毁
  • 直到下一次 a 函数重新执行时,会新创建新的 AO 对象,保存对 b 函数的新声明,产生一个全新的 b 函数的 AO

(五)立即执行函数、闭包

1.例子引入

function a() {
  function b() {
    var bbb = 234;
    console.log(aaa);
  }
  var aaa = 124;
  return b;
}
var glob = 100;
var demo = a();
demo(); // 124
  • a 直到执行完毕前,b 都没有被执行,所以此时 b 的 [[scope]] 保存的就是 a 执行是的 [[scope]]

2.副作用

  • 内部函数被保存到外部时,必定产生闭包
  • 闭包会导致原有作用域链不释放,造成内存泄漏

3.作用

1)实现公有变量

  • 如:函数累加器
// 累加器
function add() {
  var count = 0;
  function demo() {
    count++;
    console.log(count);
  }
  return demo;
}
var counter = add();
counter(); // 1
counter(); // 2
counter(); // 3

2)可以作缓存

  • 外部不可见的一种存储结构
  • 如:eater
// a、b定义时保存的都是test的AO
function test() {
  var num = 100;
  function a() {
    num++;
    console.log(num);
  }
  function b() {
    num--;
    console.log(num);
  }
  return [a, b];
}
var myArr = test();
myArr[0](); // 101
myArr[1](); // 100
function eater() {
  var food = "";
  var obj = {
    eat: function () {
      console.log(food);
      food = "";
    },
    push: function (myFood) {
      food = myFood;
    },
  };
  return obj;
}
var eater1 = eater();
eater1.push("banana");
eater1.eat();

3)可以实现封装

  • 属性私有化
    • 对象自己通过本身设置的方法才能操作的变量
    • 外部无法通过对象调用该变量
  • 如:Person();
function Deng(name, wife) {
  var prepareWife = "XiaoZhang"; // 私有化变量
  this.name = name;
  this.wife = wife;
  this.divorce = function () {
    this.wife = prepareWife;
  };
  this.changePrepareWife = function (target) {
    prepareWife = target;
  };
  this.sayPrepareWife = function () {
    console.log(prepareWife);
  };
}
var deng = new Deng("deng", "XiaoLiu");
console.log(deng.prepareWife); // undefined
deng.divorce();
console.log(deng.wife); // XiaoZhang

4)模块化开发

  • 防止污染全局变量

4.立即执行函数

  • 此类函数没有声明,在一次执行过后就释放
  • 适合做初始化工作
    • 除了初始化页面,后续需要用到的数据都需要被返回

1)形式一:(function() {}())

  • W3C 建议使用该语法
(function (x, y, z) {
  var a = 123;
  var b = 234;
  console.log(a + b);
})();
  • 计算某个值,后续只使用这个值,不需要计算过程
var num = (function (x, y, z) {
  var a = 123;
  var b = 234;
  return a + b;
})();

2)形式二:(function() {})()

  • 只有表达式才能被执行符号执行:test();
  • 能被执行符号执行的函数表达式,其函数名自动被忽略
    • 此时输出函数名为 undefined,因为该函数已变成立即执行函数
// 以下语法错误,函数声明不能被执行符号执行
function test() {
  var a = 123;
}()

// 可以执行,这是函数表达式
var test = (function () {
  console.log("a");
})();
console.log(test); // undefined
  • 只要把函数声明转变为表达式,也可以被执行符号执行
// 正号
+ function test() {
  console.log('a');
}();

// 负号
- function test() {
  console.log('a');
}();

// 逻辑非
! function test() {
  console.log('a');
}();

// 逻辑与
(...其他表达式) && function test() {
  console.log('a');
}();

// 逻辑或
(...其他表达式) || function test() {
  console.log('a');
}();

3)阿里巴巴笔试题

  • 系统编译时保持能不报错就不报错
  • 以下代码不运行也不报错
/*
function test(a, b, c, d) {
  console.log(a + b + c + d);
}(1, 2, 3, 4);
*/

// 编译为
/*
function test(a,b,c,d) {
  console.log(a+b+c+d);
}
(1,2,3,4);
*/

5.解决闭包产生的问题

1)问题引入

function test() {
  var arr = [];
  for (var i = 0; i < 10; i++) {
    /*
      函数赋值到数组中,并没有执行当前函数
      所以数组赋值时的i是for循环的i(变现),但是赋值的function内部输出的i不是for循环的i(不变现,还是未知数)
      test函数执行时遍历的function的i是最终闭包中的i
      闭包中的i=9,i++ => 10,判断<10不满足,退出循环,所以是10
    */
    arr[i] = function () {
      console.log(i); // 10 10 10 10 10 10 10 10 10 10
    };
  }
  return arr;
}
var myArr = test();
for (var j = 0; j < 10; j++) {
  myArr[j]();
}

2)用闭包解决闭包

function test() {
  var arr = [];
  for (var i = 0; i < 10; i++) {
    /*
      生成10个立即执行函数
      每次循环将变现的i保存到k中
      10个立即执行函数保存了10个不同的i值,所以正常输出
    */
    (function (k) {
      arr[k] = function () {
        console.log(k); // 0 1 2 3 4 5 6 7 8 9
      };
    })(i);
  }
  return arr;
}
var myArr = test();
for (var j = 0; j < 10; j++) {
  myArr[j]();
}

(六)对象、包装类

1.对象的创建方法

1)对象字面量/对象直接量

var obj = {};

2)构造函数

  • 系统自带的构造函数
var obj = new Object();
  • 自定义的构造函数
    • 结构上和函数完全一致
    • 但必须经过 new 调用才会返回对象
  • new 调用步骤:
    • 在构造函数顶部隐式创建 this 的对象(仅包含 __proto__
    • 之后往 this 身上添加属性名和属性值
    • 最后在构造函数底部隐式返回 this 对象
      • 如果自定义 return,只有返回 object 时会覆盖 this,返回其他原始值或数组都无效(照样返回 this)
      • 所以 使用 new 就只能返回对象
function Person() {
  // var this = {}; // AO { this: {} }
  this.属性 = 属性值;
  // return this;
}
var person1 = new Person();

警告

为了避免和普通函数混淆,应采用 大驼峰式命名规则

3)Object.create(原型)

var obj = {
  name: "Sunny",
  age: 23,
};
var obj1 = Object.create(obj);
obj1.__proto__ = obj;

2.包装类

1)引入

  • 原始值明确规定不允许有属性和方法
  • 但是可以调用 .length 输出长度
  • 原理:内部进行了包装类的转换
var num = 4;
// 隐式转换:new Number(4).len = 3; delete
num.len = 3;
// 隐式转换:new Number(4).len
console.log(num.len); // undefined,因为数字没有len属性

var str = "abcd";
// new String('abcd').length = 2; delete
str.length = 2;
console.log(str); // abcd
console.log(str.length); // 4,因为字符串本身就有length属性

2)几个包装类

  • 数字对象 new Number()
    • 普通的数字通过包装类赋值后变成对象
    • 可以增删改查其内部属性
    • 可以进行计算
      • 但是计算后恢复原来的数字,不再是对象
  • 字符串对象 new String()
    • 特性同上
  • 布尔对象 new Boolean()
    • 特性同上
// var num = 123; // 普通数字
var num = new Number(123); // 数字对象
console.log(num); // Number {[[PrimitiveValue]]: 123}
num.abc = "a";
console.log(num.abc); // "a"
console.log(num); // Number {abc: "a", [[PrimitiveValue]]: 123}
console.log(num * 2); // 246

var str = new String("abcd");
str.a = "bcd";
console.log(str.a); // "bcd"

3)练习题

var str = "abc";
str += 1;
var test = typeof str; // "string"
if (test.length == 6) {
  // new String(test).sign = '...'; delete
  test.sign = "typeof的返回结果可能是String";
}
// new String(test).sign
console.log(test.sign); // undefined
function Person(name, age, sex) {
  var a = 0;
  this.name = name;
  this.age = age;
  this.sex = sex;
  function sss() {
    a++;
    console.log(a);
  }
  this.say = sss;
}
var oPerson = new Person();
oPerson.say(); // 1
oPerson.say(); // 2
var oPerson1 = new Person();
oPerson1.say(); // 1
var x = 1,
  y = (z = 0);
function add(n) {
  return (n = n + 1);
}
y = add(x);
function add(n) {
  return (n = n + 3);
}
z = add(x);
console.log(x); // 1
console.log(y); // 4
console.log(z); // 4

3.可配置的属性

  • Object.create(prototype, definedProperties)
    • 第二个参数可以配置原型上的某些属性的特性
    • 可读、可写、可枚举、可配置
  • 一旦经历了 var 的操作,所得出的属性会提升到 window 对象中
    • 这种属性就叫做不可配置的属性
  • 不可配置的属性无法 delete
var obj = {};
obj.num = 234;
delete obj.num; // true
console.log(obj); // {}

// 直接赋值,没有经过 var
window.name = "a";
delete window.name; // true
console.log(window.name); // undefined

// 经过var的赋值
var age = 20;
delete window.age; // false
console.log(window.age); // 20

(七)原型、原型链

1.原型 prototype

  • 原型是 function 的一个属性,它定义了构造函数制造出的对象的公共祖先
    • 通过该构造函数产生的对象,可以继承该原型的属性和方法
    • 原型也是对象,默认为空对象
  • 利用原型特点和概念,可以提取共有属性
  • 对象查看原型:隐式属性 obj.__proto__
  • 对象查看构造函数:obj.constructor
// Person.prototype 原型
// Person.prototype = {}; 是祖先
Person.prototype.lastName = "Deng";
function Person() {}

2.隐式属性 __proto__

  • new 创建对象时,会在构造函数顶部隐式添加 this
  • 当 new 创建出来的对象访问某个属性,且构造函数内部没有定义该属性时
    • 会自动沿着 __proto__ 指向的值的构造函数寻找该属性
    • obj.__proto__ 可以修改对象的原型,但系统不建议修改
function Person() {
  // var this = {
  //   __proto__: Person.prototype
  // }
}
Person.prototype.name = "Sunny";
var person = new Person();
console.log(person.name); // person没有name => Person.prototype.name => Sunny

// 相当于 Person.prototype 换了个内存空间,不再指向原来的地址,但是 this中的__proto__存储的依旧是原来的地址,所以输出仍为原来的值
Person.prototype = {
  name: "Cherry",
};
console.log(person.name); // Sunny
// // 类比
// var obj = { name: "a" };
// var obj1 = obj;
// obj = { name: "b" };
// console.log(obj1); // {name: 'a'}

3.原型链

  • 原型指向关系形成链式关系,就是原型链
    • 按照 prototype 一级一级往上查找属性
  • 所有对象的最上级原型是 Object
    • Object 有 toString()valueOf()
  • 绝大多数 对象的最终都会继承自 Object.prototype
    • 使用 Object.create() 创建的对象例外,有可能原型是 null
    • undefined 和 null 不是原始值,不是对象,没有经过包装类,所以没有 toString() ,即没有原型
Grand.prototype.__proto__ = Object.prototype;

Grand.prototype.lastName = "Deng";
function Grand() {}
var grand = new Grand();

Father.prototype = grand;
function Father() {
  this.name = "Xuming";
}
var father = new Father();

Son.prototype = father;
function Son() {
  this.hobbit = "eat";
}
var son = new Son();

4.JS 的小 Bug

  • 计算精度不准 0.14 * 100 => 14.000000000000002
  • 能正常计算的位数:[小数点前16位, 小数点后16位]
  • 所以要避免小数操作
  • 无法避免时,应使用以下函数取整
    • Math.ceil() 向上取整
    • Math.floor() 向下取整
Math.ceil(123.234); // 124
Math.floor(123.999); // 123

// 以下输出有精度不准的偏差(不是toFixed的问题)
// for (var i = 0; i < 10; i++) {
//   var num = Math.random().toFixed(2) * 100;
//   console.log(num);
// }
// 一般先取整再保留两位小数
for (var i = 0; i < 10; i++) {
  var num = Math.floor(Math.random() * 100).toFixed(2);
  console.log(num);
}

5.call() / apply()

  • 作用:改变 this 指向
  • 区别:后面传的参数形式不同

1)call

  • 任何一个方法都可以执行 call()
  • test() ==> test.call()
  • 构造函数中的 this 在 new 之前默认指向 window
    • new 之后指向当前声明的对象(谁调用函数方法,this 就指向谁)
    • call(对象,参数 1,参数 2,...)可以把原构造函数中预设的 this 值全部改为指向参数传的对象
      • 用别人的方法给自己赋值
      • 参数是别人的构造方法需要的参数(按照形参个数和顺序把实参传进去)
function Person(name, age) {
  this.name = name;
  this.age = age;
}
var person = new Person("Deng", 100);
var obj = {};
Person.call(); // 不传参数和正常执行Person()构造函数一样
Person.call(obj, "Cheng", 300); // 将Person构造函数中的this改成指向obj
console.log(obj); // {name: "Cheng", age: 300}
  • 使用 call 可以引用别人写好的构造函数
function Person(name, age, sex) {
  this.name = name;
  this.age = age;
  this.sex = sex;
}
function Student(name, age, sex, tel, grade) {
  // this.name = name;
  // this.age = age;
  // this.sex = sex;
  Person.call(this, name, age, sex);
  this.tel = tel;
  this.grade = grade;
}
var student = new Student("Sunny", 123, "male", 139, 2017);

2)apply

  • 使用和 call 基本一致
  • 区别:只能传递一个参数,且必须是数组(传递一个 arguments)
Person.apply(this, [name, age, sex]);

(八)继承模式、命名空间、对象枚举

1.继承发展史

  • 传统形式——原型链
    • 过多的继承了没用的属性
  • 借用构造函数——call/apply
    • 不能继承借用构造函数的原型
    • 每次构造函数都要多调用一个函数
  • 共享原型/公有原型
    • Son.prototype = Father.prototype
    • 不能随便改动自己的原型
  • 圣杯模式
    • 声明新的构造函数 F 继承自公有原型 Father,目标构造函数 Son 再继承自构造函数 F
    • 目标构造函数 Son 修改自身原型就不会影响到其他继承自公有原型的构造函数
    • 但是 Son 本身的构造函数 constructor 指向 Father
      • son.__proto__ => new F().__proto__ => Father.prototype

1)公有原型

function Father() {}
function Son() {}
function inherit(Target, Origin) {
  Target.prototype = Origin.prototype;
}
inherit(Son, Father);
Son.prototype.sex = "male";
var father = new Father();
var son = new Son();
console.log(father.sex); // "male",Son改的原型,Father也能用

2)圣杯模式

/**
 * Father.prototype
 * => function F() {} F.prototype = Father.prototype
 * => Son.prototype = new F();
 */
function Father() {}
function Son() {}
function inherit(Target, Origin) {
  function F() {}
  F.prototype = Origin.prototype;
  Target.prototype = new F(); // 必须先改原型再new
  // 目标构造函数归位
  Target.prototype.constructor = Target;
  // 标记目标构造函数的超类super(真正继承自谁,不是自定义的F)
  Target.prototype.uber = Origin.prototype; // super是关键字,改名uber
}
inherit(Son, Father);
Son.prototype.sex = "male";
var father = new Father();
var son = new Son();
console.log(father.sex); // undefined

实现圣杯模式

  • 通俗方式
function inherit(Target, Origin) {
  function F() {}
  F.prototype = Origin.prototype;
  Target.prototype = new F();
  Target.prototype.constructor = Target;
  Target.prototype.uber = Origin.prototype;
}
  • 雅虎开源库 YUI3
var inherit = (function () {
  // F 形成闭包,称为inherit的私有化变量,不会被外部访问到
  var F = function () {};
  return function (Target, Origin) {
    F.prototype = Origin.prototype;
    Target.prototype = new F();
    Target.prototype.constructor = Target;
    Target.prototype.uber = Origin.prototype;
  };
})();

2.命名空间

  • 管理变量,防止污染全局,适用于模块化开发

1)旧方法

var org = {
  department1: {
    jicheng: {
      name: "abc",
      age: 123,
    },
    xuming: {},
  },
  department2: {
    zhangsan: {},
    lisi: {},
  },
};
var jicheng = org.department1.jicheng;
console.log(jicheng.name);

2)新方法 —— 闭包

var init = (function () {
  var name = "abc";
  function callName() {
    console.log(name);
  }
  return function () {
    callName();
  };
})();

3.访问对象方法

1)普通调用

var obj = {
  fn1: function () {},
  fn2: function () {},
  fn3: function () {},
};
// obj.fn1();
// obj.fn2();
// obj.fn3();

2)连续调用

var obj = {
  fn1: function () {
    return this;
  },
  fn2: function () {
    return this;
  },
  fn3: function () {
    return this;
  },
};
obj.fn1().fn2().fn3();

4.访问对象属性

  • obj.name === obj['name']

5.对象的枚举

1)for...in..

  • 通过对象的属性个数来控制循环次数
  • 会沿着原型链遍历所有属性
    • 一旦遍历到最顶层原型(Object.prototype)就不会再遍历
for (var prop in obj) {
  // console.log(obj.prop + " " + typeof prop); // 错误,obj.prop => obj['prop'],没有prop属性就会输出undefined
  console.log(obj[prop] + " " + typeof prop);
}

2)hasOwnProperty(prop)

  • 判断属性是不是自身的
  • 是则返回 true
if (obj.hasOwnProperty("name")) {
  console.log("这个name属性是obj自己的,不是继承来的");
}

3)prop in obj

  • 判断 obj 能不能访问到 prop
  • 包括原型链上可访问的属性
  • 能则返回 true
console.log("name" in obj);

4)A instanceof B

  • 判断 A 是不是由 B 的构造函数构造出来的对象
  • 实质:判断 A 的原型链上有没有 B 的原型(B.prototype
console.log(A instanceof B);

6.区分空数组和空对象的方法

1)constructor

var a = [];
var b = {};
console.log(a.constructor); // function Array() {}
console.log(b.constructor); // function Object() {}

2)instanceof

var a = [];
var b = {};
console.log(a instanceof Array); // true
console.log(b instanceof Array); // false

3)Object.prototype.toString.call()

  • 这个方法内部有 this,谁调用该方法,this 就指向谁【识别 this,返回相应的结果】
var a = [];
var b = {};
console.log(Object.prototype.toString.call(a)); // "[object Array]"
console.log(Object.prototype.toString.call(b)); // "[object Object]"

(九)this

1.函数预编译过程 this 指向 window

function test(c) {
  var a = 123;
  function b() {}
}
/**
 * AO {
 *  arguments: [1],
 *  this: window,
 *  c: 1,
 *  a: undefined,
 *  b: function() {}
 * }
 */
test(1);
  • 如果把 test 当作构造函数,使用 new 调用,则会覆盖默认的 this 指向
function Test(c) {
  // var this = Object.create(test.prototype);
  // => this: { __proto__: test.prototype }
  var a = 123;
  function b() {}
}
var t = new Test();

2.全局作用域的 this 指向 window

/**
 * GO {
 *  this: window
 * }
 */

3.call/apply 可以改变函数运行时的 this 指向

4.obj.func(); 的 func() 中的 this 指向 obj

  • 谁调用函数,函数中的 this 就指向谁
  • 没有调用而是自动执行,则 this 指向预编译时的执行环境,一般是全局 window

5.this 笔试题

var name = "222";
var a = {
  name: "111",
  say: function () {
    console.log(this.name);
  },
};
var fun = a.say;
fun(); // 222,相当于把a的say函数赋值给fun,fun在全局调用,所以this指向window
a.say(); // 111,a调用say函数,this指向a
var b = {
  name: "333",
  say: function (func) {
    // console.log(this); // b
    func();
  },
};
/**
 * b调用say函数,所以b的say函数中this指向b
 * 参数a.say相当于把a的say函数放在b的say函数中执行
 * 由于b的say函数是直接执行参数func,并不是 `this.func()`
 * 所以func函数没有被谁调用,走的是预编译流程,此时this指向window
 */
b.say(a.say); // 222
b.say = a.say;
b.say(); // 333,相当于把a的say函数赋值给b的say属性,此时this指向b
var foo = 123;
function print() {
  this.foo = 234;
  console.log(foo);
}
print(); // 234,this指向window,打印的是全局的foo
var foo = 123;
function print() {
  // var this  = Object.create(print.prototype);
  this.foo = 234;
  console.log(foo);
}
new print(); // 123,通过new创建对象,构造函数内部的this被覆盖,但是输出的foo不是构造函数的属性,是全局的变量
function print() {
  var marty = {
    name: "marty",
    printName: function () {
      console.log(this.name);
    },
  };
  var test1 = {
    name: "test1",
  };
  var test2 = {
    name: "test2",
  };
  var test3 = {
    name: "test3",
  };
  test3.printName = marty.printName;
  var printName2 = marty.printName.bind({
    name: 123,
  });
  marty.printName.call(test1); // test1
  marty.printName.apply(test2); // test2
  marty.printName(); // marty
  printName2(); // 123
  test3.printName(); // test3
}
print();
var bar = {
  a: "002",
};
function print() {
  bar.a = "a";
  Object.prototype.b = "b";
  return function inner() {
    console.log(bar.a); // a
    console.log(bar.b); // b
  };
}
// 函数返回另一个函数,返回后立即执行这个函数
print()();

(十)arguments、克隆、三目运算符、数组、类数组

1.arguments

1)arguments.callee

  • 指向函数自身的引用
  • arguments 只有两个属性
    • callee
    • length
function test() {
  console.log(arguments.callee); // test
}
test();
console.log(arguments.callee == test); // true
  • 应用:匿名函数需要自身的引用
var num = (function (n) {
  if (n == 1) return 1;
  return n * arguments.callee(n - 1);
})(100);

2)func.caller

  • 指向函数的执行环境
  • 是函数自身的属性
  • 在 ES5 的严格模式下会报错
function test() {
  demo();
}
function demo() {
  console.log(demo.caller);
}
test(); // function test() {}

2.克隆

1)浅克隆

  • 克隆原始值属性
    • 修改源对象的值,目标对象值不变
  • 克隆引用值属性
    • 修改源对象的值,目标对象值也改变
var obj = {
  name: "John",
  age: 20,
  sex: "male",
  master: ["a", "b"],
};
function clone(origin, target) {
  var target = target || {};
  for (var prop in origin) {
    target[prop] = origin[prop];
  }
  return target;
}
var obj1 = clone(obj, obj1);
console.log(obj, obj1); // { name: 'John', age: 20, sex: 'male', master: [ 'a', 'b' ] } { name: 'John', age: 20, sex: 'male', master: [ 'a', 'b' ] }

obj.name = "Lucy";
console.log(obj.name, obj1.name); // Lucy John

obj.master.push("c");
console.log(obj.master, obj1.master); // ['a', 'b', 'c'] ['a', 'b', 'c']

2)深克隆

  • 克隆原始值属性
    • 修改源对象的值,目标对象值不变
  • 克隆引用值属性
    • 修改源对象的值,目标对象值不变
/**
 * 1.判断是原始值还是引用值
 * 2.如果是引用值,判断是数组还是对象
 * 3.如果是数组就给当前属性赋值空数组,如果是对象就给当前属性赋值空对象
 * 4.把新的空数组或空对象当作新的目标对象,从源对象的属性再次进行克隆(递归)
 */
var obj = {
  name: "John",
  age: 20,
  sex: "male",
  master: ["a", "b"],
  friends: {
    a: "sss",
    b: "ddd",
    c: ["e", "f"],
  },
};
function deepClone(origin, target) {
  var target = target || {},
    toStr = Object.prototype.toString,
    arrStr = "[object Array]";
  for (var prop in origin) {
    // 只判断当前对象的属性值,不判断其原型链上的属性
    if (origin.hasOwnProperty(prop)) {
      if (origin[prop] !== "null" && typeof origin[prop] === "object") {
        // 是引用值
        // if (toStr.call(origin[prop]) === arrStr) {
        //   // 是数组
        //   target[prop] = [];
        // } else {
        //   // 是对象
        //   target[prop] = {};
        // }
        target[prop] = toStr.call(origin[prop]) === arrStr ? [] : {};
        // 递归
        deepClone(origin[prop], target[prop]);
      } else {
        // 是原始值
        target[prop] = origin[prop];
      }
    }
  }
  return target;
}
var obj1 = {};
deepClone(obj, obj1);
console.log(obj, obj1);

obj.name = "Lucy";
console.log(obj.name, obj1.name); // Lucy John

obj.master.push("c");
console.log(obj.master, obj1.master); // [ 'a', 'b', 'c' ] [ 'a', 'b' ]

3.三目运算符

  • 格式:条件判断 ? 是 : 否
  • 有返回值
var num = 1 > 0 ? 2 + 2 : 1 + 1;
console.log(num); // 4

4.数组

1)定义

  • 字面量 arr = [];
    • 用逗号隔开但不是每一位都有值(undefined)
    • arr = [1,2,,,,3] => arr.length = 6
  • new Array(length/content)
    • 如果传多个参数,和字面量形式一样,表示的是数组各个位数的值
    • 如果只传一个参数,表示的是数组的长度,且所有位数值都为 undefined
      • 只能传整数,否则报错

2)读和写

  • arr[num]
    • 不可以溢出读
    • 不报错,但值是 undefined
  • arr[num] = xxx;
    • 可以溢出写,数组长度自动撑长

3)改变原数组的方法(ES3.0)

注意

能改变原数组的方法只有以下几个

push、pop、shift、unshift、sort、reverse、splice

ES5、ES6 之后新增的方法也不能改变原数组

  • push
    • 添加元素并返回数组长度
Array.prototype.push = function () {
  for (var i = 0; i < arguments.length; i++) {
    // 谁调用push方法,this就指向谁
    // 每次都给数组最后一位赋值
    this[this.length] = arguments[i];
  }
  return this.length;
};
  • pop
    • 删除元素并返回
var arr = [1, 3, 4];
arr.pop();
console.log(arr); // [1,3]
  • shift
    • 往数组最后一位插入元素
var arr = [1];
arr.shift(3);
console.log(arr); // [1,3]
  • unshift
    • 往数组第一位插入元素
var arr = [1, 2];
arr.unshift(0);
console.log(arr); // [0,1,2]
  • sort
    • 将数组元素进行排序
    • 默认按照 ASCII 编码排序
    • 如果需要按数字排序,需要传入处理函数作为参数
      • 执行 sort 时,依次从数组中取出两位,分别传入处理函数作为实参,返回值即当前两位数的排序结果
        • 取数按照冒泡排序规则:第 1 位依次和第 2 位之后的几位比较排序,之后第 2 位再和第 3 位之后的几位比较排序,依次执行到最后两位比较完
      • 必须写两个形参
      • 看返回值
        • 返回值为负数时,参数 1 放在前面
        • 返回值为正数时,参数 2 放在前面
        • 返回值为 0 时,参数不动
var arr = [1, 2, 3, 5, 4, 10];
arr.sort();
console.log(arr); // [1,10,2,3,4,5]

arr.sort(function (a, b) {
  // if (a > b) return 1;
  // else if (a < b) return -1;

  // if (a - b > 0) return 1;
  // else if (a - b < 0) return -1;

  // return a - b; // 升序
  return b - a; // 降序
});
/**
 * 给定一个有序的数组,输出乱序排序后的结果
 * 利用Math.random()随机返回一个(0,1)之间的数
 * 随机生成的这个数减去0.5就有可能返回正数、负数、0
 * 通过sort参数的返回值规则实现乱序排序
 */
var arr = [1, 2, 3, 4, 5, 6, 7];
arr.sort(function () {
  return Math.random() - 0.5;
});
console.log(arr);
  • reverse
    • 将数组元素颠倒顺序
var arr = [1, 2, 3];
arr.reverse();
console.log(arr); // [3,2,1]
  • splice
    • 数组切片
    • 参数 1:从第几位开始截取
      • 如果是负数则从数组末端开始数
    • 参数 2:截取字符串的长度
    • 参数 3 及之后:在切口处添加的新元素
      • 传多少个参数就添加多少个新元素
    • 返回截取后的数组
Array.prototype.splice = function (pos) {
  // ...
  // 负数倒数的原理 —— 当前下标+数组长度
  pos += pos > 0 ? 0 : this.length;
  // ...
};

4)不改变原数组的方法(ES3.0)

  • concat
    • 拼合两个数组,返回拼合后的新数组
    • 保留原来两个数组
var arr1 = [1, 2, 3];
var arr2 = [5, 6, 7];
console.log(arr1.concat(arr2)); // [1,2,3,5,6,7]
console.log(arr1); // [1,2,3]
console.log(arr2); // [5,6,7]
  • join
    • 传入连接字符
      • 不传默认使用英文逗号 , 连接
      • 也可以传入空串 "" ,则不使用额外字符将数组转成连续字符串
    • 将数组元素使用该字符连接后转成字符串输出
    • 是 split 的逆操作
var arr = [1, 2, 3, 4, 5];
arr.join("-");
console.log(arr); // "1-2-3-4-5"
  • split
    • 是字符串的方法
    • 传入分割字符
    • 将字符串按照该字符分割后转成数组输出
    • 是 join 的逆操作
var str = "1~2~3~4~5";
str.split("~");
console.log(str); // ["1", "2", "3", "4", "5"]
  • toString
    • Array 内部实现的方法
    • 将数组所有元素转成字符串
var arr = [1, 2, 3, 4, 5];
console.log(arr.toString()); // "1,2,3,4,5"
  • slice
    • 截取当前数组,返回截取出来的数组片段
    • 保留原来的数组
    • 形式 1:两个参数
      • arr.slice(从哪位开始截取, 截取到哪一位)
    • 形式 2:一个参数
      • arr.slice(从哪位开始截取)
      • 截取到数组最后一位
      • 正数则第 1 位是 0
      • 负数则最后 1 位是-1
    • 形式 3:没有参数
      • arr.slice()
      • 截取整个数组(相当于拷贝)
var arr = [1, 2, 3, 4, 5, 6];
console.log(arr.slice(1, 2)); // [2]

5.类数组

1)定义

  • 类似数组
  • 但是不能调用数组方法
  • 如:arguments

2)组成

  • 属性要为索引(数字)属性
  • 必须有 length 属性
    • 因为数组方法操作时关注的是 length
  • 最好加上 push 方法
var obj = {
  0: "a",
  1: "b",
  2: "c",
  length: 3,
  push: Array.prototype.push,
};
obj.push("d");
console.log(obj); // Object {0: 'a', 1: 'b', 2: 'c', 3: 'd', length: 4}
  • 如果加上 splice 方法,就长得和数组一样
  • 可以当作对象使用,也可以当作数组使用
  • 只能使用已添加为属性的数组方法,没添加的无法使用
var obj = {
  0: "a",
  1: "b",
  2: "c",
  name: "Lucy",
  age: 20,
  length: 3,
  push: Array.prototype.push,
  splice: Array.prototype.splice,
};
console.log(obj); // ['a', 'b', 'c']
console.log(obj.length); // 3
console.log(obj.name, obj.age); // Lucy 20
  • 阿里巴巴笔试题
var obj = {
  2: "a",
  3: "b",
  length: 2,
  push: Array.prototype.push,
};
// Array.prototype.push = function (target) {
//   obj[obj.length] = target;
//   obj.length ++;
// }
obj.push("c");
obj.push("d");
console.log(obj); // Object { 2: "c", 3: "d", length: 4}

3)特性

  • 可以利用属性名模拟数组的特性
  • 可以动态增长 length 属性
  • 如果强行让类数组调用 push 方法,则会根据 length 属性值的位置进行属性的扩充

(十一)try...catch、ES5 标准模式

1.try...catch

  • 在 try 里面的代码执行出错
    • 出错的代码行之后的 try 代码不执行
    • 继续执行 catch、finally 和 try 以外的代码
  • try 里面代码没有错误,不会执行 catch 的代码

1)语法

try {
  // ...
} catch (e) {
  console.log(e.name + ":" + e.message);
} finally {
  // ...
}

2)Error.name 的六种值对应的信息

  • EvalError
    • eval()的使用与定义不一致
  • RangeError
    • 数值越界
  • ReferenceError
    • 非法或不能识别的引用数值
  • SyntaxError
    • 发生语法解析错误
  • TypeError
    • 操作数类型错误
  • URIError
    • URI 处理函数使用不当

2.ES5 的严格模式

1)背景

  • 浏览器是基于 ES3 的语法+ES5 新增的方法 开发的
  • 对于 ES3 和 ES5 语法中产生冲突的部分
    • 如果开启严格模式,那么使用 ES5 的语法
      • 不再兼容 ES3 的一些不规则语法,使用全新的 ES5 规范
    • 如果不开启严格模式(标准模式),则使用 ES3 的语法

2)严格模式

  • 本质是一行字符串,不会对不兼容严格模式的浏览器产生影响
  • 不支持 witharguments.calleefunc.caller
  • 变量赋值前必须声明
    • 暗示全局变量不可用
    • var a = b = 3; b 报错未定义
  • 全局 this 默认还是指向 window
  • 局部 this 必须被赋值
    • 预编译时的 this 不指向 window,而是 undefined
    • Person.call(null/undefined/其他)
      • 参数赋值什么,this 就是什么
  • 拒绝重复属性和参数

3)全局严格模式

  • 在 JS 文件或 script 代码块的顶端添加
"use strict";
function test() {
  console.log(arguments.callee); // 报错
}
test();

4)局部函数内严格模式【推荐】

  • 在局部函数的第一行添加
function demo() {
  console.log(arguments.callee); // function demo() { ... }
}
demo();
function test() {
  "use strict";
  console.log(arguments.callee); // 报错
}
test();

5)with()

  • 如果 with 传入一个对象
    • 会把这个对象当作 with 所圈定的代码体的作用域链的最顶端
    • 即第一个 AO
    • 当 with 所圈定的代码体中访问了某些属性,会先在这个对象身上找,找不到再顺着作用域链找上一级 AO 对象
  • 本质就是修改对象的作用域链
    • 会导致程序运行变慢
    • 所以严格模式禁用
var obj = {
  name: "obj",
};
var name = "window";
function test() {
  var name = "scope";
  var age = 123;
  with (obj) {
    // 先在obj的AO对象上找name属性,找不到再找test函数的AO对象
    console.log(name); // obj
    console.log(age); // 123
  }
}
test();
var org = {
  dp1: {
    jc: {
      name: "abc",
      age: 123,
    },
    deng: {
      name: "def",
      age: 456,
    },
  },
  dp2: {},
};
with (org.dp1.jc) {
  console.log(name); // abc
}
with (org.dp1.deng) {
  console.log(name); // def
}

6)eval()

  • 传入一个字符串
  • 会将字符串当作代码执行
eval('console.log('aaa')'); // aaa
  • 通用规则:ES3 中不能使用 eval
  • 因为 eval 能改变作用域
    • 不同条件下,改变的作用域不同
    • eval 还有自身的作用域

(十二)DOM

1.定义

  • DOM —— Document Object Model 文档对象模型
  • 定义了表示和修改文档所需的方法
  • DOM 对象即宿主对象,由浏览器厂商定义, 用于操作 HTML 和 XML 功能 的一类对象的集合
    • 也有人称 DOM 是对 HTML 及 XML 的标准编程接口
  • DOM 不能直接操作 CSS 样式表
    • dom.style.xxx 是间接给 DOM 对象增加行内样式从而修改了 CSS 样式
    • JS 中的样式属性名必须使用小驼峰命名方式 backgroundColor
  • API 生成的数组基本都是类数组

相关信息

XML -> XHTML -> HTML

XML 和 HTML 基本一致,但是 XML 可以自定义标签名

早期利用 XML 自定义标签名的特性来传输对象格式的数据,现在改为用 JSON 传输

2.节点

1)类型

  • 获取节点类型:nodeType
名称
元素节点1
属性节点2
文本节点(包括换行)3
注释节点8
document9
DocumentFragment11
<div>
  123
  <!-- This is comment -->
  <strong></strong>
  <span></span>
</div>
var div = document.getElementsByTagName("div")[0];
console.log(div.childNodes.length); // 7
// 换行到注释节点开始前都算作一个文本节点
// [文本节点、注释节点、文本节点、元素节点、文本节点、元素节点、文本节点]

console.log(document.nodeType); // 9

2)四个属性

  • nodeName
    • 元素的标签名
    • 以大写形式表示
    • 只读
  • nodeValue
    • Text 节点或 Comment 节点的文本内容
    • 可读写
  • nodeType
    • 该节点的类型
    • 只读
  • attributes
    • Element 节点的属性集合

3)一个方法

  • Node.hasChildNodes()
    • 判断节点有没有子节点
    • 返回 boolean
    • 空格回车都算有子节点

3.对节点的增删改查

1)查看——查看元素节点

  • document
    • 代表整个文档
  • document.getElementById()
    • 元素 id 在 IE8 以下的浏览器,不区分大小写,且返回匹配 name 属性的元素
  • document.getElementsByTagName()
    • 标签名
  • document.getElementsByName()
    • 只有部分标签 name 可生效(可提交的属性名)
    • 表单、表单元素、img、iframe
  • document.getElementsByClassName()
    • 类名
    • IE8 及以下版本的 IE 浏览器没有,一般用 getElementsByTagName
    • 可以多个 class 一起查找
  • document.querySelector()
    • CSS 选择器
      • "div > p .demo"
    • 选一个
    • IE7 及以下版本的 IE 浏览器没有
    • 选中的元素不是实时的,选出来的其实是元素的副本【镜像】
      • 选完后,元素新增删除,结果集都不变化
  • document.querySelectorAll()
    • CSS 选择器
      • "div > p .demo"
    • 选一组
    • IE7 及以下版本的 IE 浏览器没有
    • 选中的元素不是实时的,选出来的其实是元素的副本【镜像】
      • 选完后,元素新增删除,结果集都不变化

2)查看——遍历节点树

  • parentNode
    • 父节点
    • 最顶端为 #document
  • childNodes
    • 子节点们
  • firstChild
    • 第一个子节点
  • lastChild
    • 最后一个子节点
  • nextSibling
    • 后一个兄弟节点
  • previousSibling
    • 前一个兄弟节点

3)查看——基于元素节点树的遍历

  • parentElement
    • 返回当前元素的父元素节点
    • IE 不兼容
  • children
    • 只返回当前元素的元素子节点
    • 返回的类数组实时更新,如果遍历时有 remove 操作,下一次遍历获取到的下标有可能超出数组长度,变成 undefined
  • node.childElementCount === node.children.length
    • 当前元素节点的子元素节点个数
    • IE 不兼容
  • firstElementChild
    • 返回第一个元素节点
    • IE 不兼容
  • lastElementChild
    • 返回最后一个元素节点
    • IE 不兼容
  • nextElementSibling
    • 返回后一个兄弟元素节点
    • IE 不兼容
  • previousElementSibling
    • 返回前一个兄弟元素节点
    • IE 不兼容

注意

IE 不兼容 指的都是 IE9 及以下版本的 IE 浏览器

IE10 及以上用了新的内核(Edge),遵循的规范标准基本类似 Chrome

IE6 和 IE8 的 Bug 最多

4)增加

  • document.createElement()
    • 创建元素节点
  • document.createTextNode()
    • 创建文本节点
  • document.createComment()
    • 创建注释节点
  • document.createDocumentFragment()
    • 创建文档碎片节点

5)插入

  • parent.appendChild()
    • 任何一个元素节点都有该方法
    • 如果元素本来不存在,则功能等同于 push
    • 如果元素已经存在,插入到已有节点,则功能等同于剪切
  • parent.insertBefore(a, b)
    • 读法:insert a before b
    • 在父节点层级下,将 a 节点插入到 b 节点前面

6)删除

  • parent.removeChild()
    • 父节点删除子节点
    • 返回被删除的子节点
    • 其实就是剪切
  • child.remove()
    • 子节点自己删除
    • 返回 undefined

7)替换

  • parent.replaceChild(new, origin)
    • 读法:用 new 节点替换 origin 节点

4.DOM 继承树

  • 用于代表一系列的继承关系(原型链)

1)Node

  • 特殊的构造函数

2)Document

  • 特殊的构造函数
  • 只能由系统调用
HTMLDocument.prototype -> document
HTMLDocument.prototype = {
  __proto__: Document.prototype
}
// =>表示继承自
document => HTMLDocument.prototype => Document.prototype

相关信息

document.__proto__ = HTMLDocument.prototype;
document.__proto__.__proto__ = Document.prototype;
document.__proto__.__proto__.__proto__ = Node.prototype;
document.__proto__.__proto__.__proto__.__proto__ = EventTarget.prototype;
document.__proto__.__proto__.__proto__.__proto__.__proto__ = Object.prototype;
document.__proto__.__proto__.__proto__.__proto__.__proto__.__proto__ = null;

3)Element

  • innerHTML
    • 获取/改变元素内部 HTML 的内容
    • 可读可写
    • 都是 HTML 结构,可以有 style
  • innerText
    • 火狐不兼容
    • 获取/改变元素内部的文本内容
    • 忽视内部多个标签,返回所有子标签内部的文本
    • 如果父元素内部有其他结构,直接赋值文本会删除所有结构
  • textContent
    • 老版本 IE 不兼容
  • ele.setAttribute(attr, val)
    • 给 ele 设置行间属性 attr,值为 val
    • 可以设置系统未定义的属性,不局限于 id、class 等
  • ele.getAttribute(attr)
    • 获取 ele 的行间属性 attr 的值
  • ele.className
    • 可以读写 ele 的 class 属性
  • ele.id
    • 可以读写 ele 的 id 属性
<div>
  <span>123</span>
  <i>456</i>
</div>
var div = document.getElementsByTagName("div")[0];
console.log(div.innerText); // "123 456"

4)DOM 基本操作

  • getElementById
    • 定义在 Document.prototype 上
    • 即 Element 节点上不能使用
  • getElementsByName
    • 定义在 HTMLDocument.prototype 上
    • 即非 html 中的 document 不能使用(如:xml document、Element)
  • getElementsByTagName
    • 定义在 Document.prototype 和 Element.prototype 上
  • HTMLDocument.prototype 定义了一些常用的属性
    • body、head 分别指代 HTML 文档中的<body><head>标签
  • Document.prototype 定义了 documentElement 属性
    • 指代文档的根元素
    • 在 HTML 文档中,它总是指代 <html> 元素
  • getElementsByClassNamequerySelectorquerySelectorAll 在 Document.prototype、Element.prototype 类中均有定义
// 选择所有标签
document.getElementsByTagName("*");

(十三)Date 对象、定时器

W3School JavaScript Tutorialopen in new window

1.日期对象 Date()

  • 系统封装好的构造函数

W3School 在线教程open in new window

  • getTime()
    • 获取自 1970 年 1 月 1 日以来的毫秒数(时间戳)
    • 1970 年 1 月 1 日是计算机的纪元时间
// 计算程序执行耗时,方便优化性能问题
let firstTime = new Date().getTime();
for (let i = 0; i < 100000000; i++);
let lastTime = new Date().getTime();
console.log(lastTime - firstTime);

2.定时器

  • 定时器计时不准
    • 只能测算大概频率
  • 是定义在全局对象 window 上的方法
    • 内部函数的 this 指向 window

1)setInterval

  • 也叫循环计时器
  • 每隔一段时间就执行一次函数
    • 该函数的名称会被忽略
  • 只会在初始运行时读取 time
    • 无法通过修改 time 达到修改定时器频率的效果
var time = 1000;
setInterval(function () {
  console.log(1);
}, time);
// time = 2000;

2)clearInterval

  • 一般需要手动清除,否则会一直循环执行
var timer = setInterval(function () {
  console.log(1);
}, 1000);
clearInterval(timer);

3)setTimeout

  • 真正的定时器
  • 推迟一段时间再执行一次函数
    • 并且只执行一次
setTimeout(function () {
  console.log(1);
}, 1000);

4)clearTimeout

  • 一般不需要手动清除,因为只执行一次
  • 接收的 timer 不会和 setInterval 重复
var timer = setTimeout(function () {
  console.log(1);
}, 1000);
clearTimeout(timer);

5)setInterval("func()", 1000)

  • 每隔一段时间执行一次字符串内的代码
  • 非正规写法,一般不用
setInterval("func()", 1000);

(十四)窗口属性、DOM 尺寸、脚本化 CSS

1.查看滚动条的滚动距离

  • IE8 及以下浏览器
    • document.body.scrollLeftdocument.body.scrollTop
    • document.documentElement.scrollLeftdocument.documentElement.scrollTop
    • 兼容性比较混乱,用时取两个值相加
    • 因为不可能存在两个同时有值的情况
  • 其他高版本浏览器
    • window.pageXOffset
    • window.pageYOffset
// 封装兼容性方法,求滚动轮滚动距离getScrollOffset()
function getScrollOffset() {
  // if (window.pageXOffset) {
  if (window.scrollX) {
    return {
      // x: window.pageXOffset,
      // y: window.pageYOffset,
      x: window.scrollX,
      y: window.scrollY,
    };
  } else {
    return {
      x: document.body.scrollLeft + document.documentElement.scrollLeft,
      y: document.body.scrollTop + document.documentElement.scrollTop,
    };
  }
}

2.查看视口的尺寸

  • window.innerWidthwindow.innerHeight
    • IE8 及以下不兼容
  • document.documentElement.clientWidthdocument.documentElement.clientHeight
    • 标准模式下,任意浏览器都兼容
    • document.compatMode = 'CSS1Compat'
  • document.body.clientWidthdocument.body.clientHeight
    • 适用于怪异模式下的浏览器
    • document.compatMode = 'BackCompat'
// 封装兼容性方法,返回浏览器的视口尺寸getViewportOffset()
function getViewportOffset() {
  if (window.innerWidth) {
    return {
      width: window.innerWidth,
      height: window.innerHeight,
    };
  } else {
    return {
      width: document.compatMode === "BackCompat" ? document.body.clientWidth : document.documentElement.clientWidth,
      height: document.compatMode === "BackCompat" ? document.body.clientHeight : document.documentElement.clientHeight,
    };
  }
}

相关信息

渲染模式:

  • 标准模式
    • <!DOCTYPE html>
    • DTD,Document Type Declarations,文档类型
  • 怪异模式/混杂模式
    • 删掉该行就是怪异模式
    • 主要作用是向后兼容,使浏览器识别非最新的语法
    • 向后兼容的版本视浏览器而定

3.查看元素的几何尺寸

  • domEle.getBoundingClientRect()
  • 兼容性很好
  • 返回一个对象,包含 left、top、bottom、right、width、height 等属性
    • left 和 top 代表该元素左上角的 X 和 Y 坐标
    • right 和 bottom 代表该元素右下角的 X 和 Y 坐标
    • width 和 height 属性,老版本的 IE 并未实现
  • 返回的结果不是实时的

4.查看元素的尺寸

  • dom.offsetWidth
  • dom.offsetHeight
  • 返回的是元素视觉上的尺寸
    • 即包含 padding,不包含 margin

5.查看元素的位置

  • dom.offsetLeftdom.offsetTop
    • 对于无定位父级的元素,返回相对文档的坐标
    • 对于有定位父级的元素,返回相对于最近的有定位的父级的坐标
    • 只要有距离就返回,无论这个距离是 margin 还是定位产生的
  • dom.offsetParent
    • 返回最近的有定位的父级
    • 若无,则返回 body

警告

body.offsetParent = null

// 求元素相对于文档的坐标getElementPosition
function getElementPosition(ele) {
  if (ele.offsetParent === document.body) {
    return {
      x: ele.offsetLeft,
      y: ele.offsetTop,
    };
  } else {
    return {
      x: ele.offsetLeft + getElementPosition(ele.parentElement),
      y: ele.offsetTop + getElementPosition(ele.parentElement),
    };
  }
}

6.让滚动条滚动

  • scroll() === scrollTo()
    • 功能类似,用法都是传入 x、y 坐标
    • 即让滚动条滚动到该坐标
  • scrollBy()
    • 传入的 x 是横向滚动距离,y 是纵向滚动距离
    • 区别:scrollBy 会在之前的数据基础上作累加
  • 三个方法都定义在 window 对象上

7.脚本化 CSS

1)读写元素的 CSS 属性

  • dom.style
    • CSSDeclaration {} 类数组
    • 所有可操作的 CSS 属性
    • 本质是操作 DOM 结构的行内样式
  • dom.style.prop
    • 可读写行间样式,没有兼容性问题
      • 如果是内联/外联样式表中设置的属性(如 width),用该值无法获取到
    • 遇到 float 这样的保留字属性,前面应该加 css
      • 不加也能生效,但不建议
    • 复合属性建议拆解
      • 不拆也能生效,但不建议
    • 组合单词变成小驼峰式写法
    • 写入的值必须是字符串格式
dom.style.cssFloat = "left";
dom.style.borderWidth = "3px";
dom.style.backgroundColor = "red";

2)查询计算样式

  • window.getComputedStyle(ele, null)
    • 计算样式只读
    • 返回的计算样式的值都是绝对值,没有相对单位
      • 获取的是当前元素所有属性的最终显示值,包括默认值
    • IE8 及以下不兼容
  • 获取伪元素样式【唯一方法】
    • window.getComputedStyle(ele, "after")
    • window.getComputedStyle(ele, "before")
  • 修改伪元素样式
    • 可以给父元素定义不同类名,对应不同样式的伪元素
    • 使用 JS 修改父元素的类,从而实现修改伪元素的样式的效果
.green::after {
  /* ... */
  background-color: green;
}
.yellow::after {
  /* ... */
  background-color: yellow;
}
var div = document.getElementsByTagName("div")[0];
div.onclick = function () {
  div.className = "yellow";
};

3)查询样式

  • ele.currentStyle
    • 计算样式只读
    • 返回的计算样式的值不是经过转换的绝对值
    • IE 独有的属性
// 封装兼容性方法查询样式getStyle(elem, prop)
function getStyle(elem, prop) {
  if (window.getComputedStyle) {
    return window.getComputedStyle(elem, null)[prop];
  } else {
    return elem.currentStyle[prop];
  }
}

注意

一般不建议多次通过 dom.style.prop 修改元素样式,有效率问题

建议把待修改的样式单独定义成一个类样式,JS 操作元素的 className 来改变样式

效率高、可维护性强

(十五)事件

1.组成

  • 事件名称
  • 事件触发时的反馈函数

2.绑定事件处理函数

1)ele.onxxx = function (event) {}

  • 兼容性很好
  • 一个元素的同一个事件上只能绑定一个处理函数
  • 基本等同于直接写在 HTML 行间上

2)obj.addEventListener(type, fn, false)

  • IE9 以下不兼容
  • 可以为一个事件绑定多个处理函数

3)obj.attachEvent('on' + type, fn)

  • IE 独有的方法
  • 一个事件同样可以绑定多个处理函数

4)面试题

<ul>
  <li>a</li>
  <li>a</li>
  <li>a</li>
  <li>a</li>
</ul>

对于以上结构,使用原生 JS 的 addEventListener,给每个 li 元素绑定一个 click 事件,点击时输出他们的顺序

var list = document.getElementsByTagName("li");
var len = list.length;
for (var i = 0; i < len; i++) {
  (function (i) {
    list[i].addEventListener(
      "click",
      function () {
        console.log(i + 1);
      },
      false,
    );
  })(i);
}

警告

绑定事件出现在 for 循环当中时,要关注是否产生闭包

3.事件处理程序的运行环境

1)ele.onxxx = function (event) {}

  • 程序 this 指向的是 dom 元素本身

2)obj.addEventListener(type, fn, false)

  • 程序 this 指向的是 dom 元素本身

3)obj.attachEvent('on' + type, fn)

  • 程序 this 指向的是 window
// 封装兼容性的绑定事件方法addEvent(elem, type, handle)
function addEvent(elem, type, handle) {
  if (elem.addEventListener) {
    elem.addEventListener(type, handle, false);
  } else if (elem.attachEvent) {
    elem.attachEvent("on" + type, function () {
      handle.call(elem);
    });
  } else {
    elem["on" + type] = handle;
  }
}

4.解除事件处理程序

  • ele.onclick = false;ele.onclick = null;ele.onclick = '';
  • ele.removeEventListener(type, fn, false);
  • ele.detachEvent('on' + type, fn);
  • 若绑定匿名函数,则无法解除
// 封装兼容性的移除事件方法removeEvent(elem, type, handle)
function removeEvent(elem, type, handle) {
  if (elem.removeEventListener) {
    elem.removeEventListener(type, handle, false);
  } else if (elem.detachEvent) {
    elem.detachEvent("on" + type, function () {
      handle.call(elem);
    });
  } else {
    elem["on" + type] = null;
  }
}

5.事件处理模型

1)事件冒泡

  • 结构上(非视觉上)嵌套关系的元素,会存在事件冒泡的功能
    • 即同一事件,自子元素冒泡向父元素
    • 自底向上

2)事件捕获

  • 结构上(非视觉上)嵌套关系的元素,会存在事件捕获的功能
    • 即同一事件,自父元素捕获至子元素(事件源元素)
      • 对于事件源元素(触发事件的元素),当前事件是“事件执行”,不是“事件捕获”
    • 自顶向下
  • IE 没有捕获事件
  • 只有 Chrome 实现了事件捕获
    • 需要修改 obj.addEventListener(type, fn, true) 最后一个参数为 true
    • 事件处理模型将变为事件捕获

3)触发顺序

  • 先捕获
  • 后冒泡

4)不冒泡的事件

  • focus
  • blur
  • change
  • submit
  • reset
  • select

5)取消冒泡

  • W3C 标准:event.stopPropagation();
    • IE9 及以下不兼容
  • IE 独有:event.cancelBubble = true;
// 封装取消冒泡的函数stopBubble(event)
function stopBubble(event) {
  if (event.stopPropagation) {
    event.stopPropagation();
  } else {
    event.cancelBubble = true;
  }
}

6)阻止默认事件

  • 默认事件:表单提交、a 标签跳转、右键菜单等
  • 以对象属性的方式注册的事件才生效:return false;
  • W3C 标准:event.preventDefault();
    • IE9 及以下不兼容
  • 兼容 IE:event.returnValue = false;
  • a 标签:<a href="javascript:void(0)">link</a>
// 封装阻止默认事件的函数cancelHandler(event)
function cancelHandler(event) {
  if (event.preventDefault) {
    event.preventDefault();
  } else {
    event.returnValue = false;
  }
}

6.事件对象

  • 用于 IE:event || window.event
  • 事件源对象:
    • Firefox 仅有 event.target
    • IE 仅有 event.srcElement
    • Chrome 两者都有
  • 兼容性写法:var target = event.target || event.srcElement;

1)事件委托

  • 利用事件冒泡和事件源对象进行处理
  • 优点:
    • 性能。不需要循环所有元素一个个绑定事件
    • 灵活。当有新的子元素时不需要重新绑定事件
<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  <!-- <li>...</li> -->
</ul>
var ul = document.getElementsByTagName("ul")[0];
ul.onclick = function (e) {
  var event = e || window.event;
  var target = event.target || event.srcElement;
  console.log(target.innerText);
};

2)捕获所有事件

  • 仅在 IE 浏览器使用
  • 将所有事件捕获到自身:elem.setCapture()
  • 释放捕获事件:ele.releaseCapture()

7.事件分类

1)鼠标事件

  • click
    • = mousedown + mouseup
    • 鼠标点击
  • mousedown
    • 鼠标按下
    • 移动端:touchstart
  • mouseup
    • 鼠标抬起
    • 移动端:touchend
  • mousemove
    • 鼠标移动
    • 移动端:touchmove
  • contextmenu
    • 右键菜单
  • mouseover
    • = mouseenter【HTML5 新规范】
    • 鼠标进入
  • mouseout
    • = mouseleave【HTML5 新规范】
    • 鼠标离开

2)鼠标按键

  • 只有 mousedown 和 mouseup 两个事件可以区分
  • 用 button 属性来区分鼠标的按键
    • 0:左键
    • 1:鼠标滚轮
    • 2:右键

注意

DOM3 标准规定:

click 事件只能监听左键

只能通过 mousedown 和 mouseup 来判断鼠标按键

  • 如何解决 mousedown 和 click 的冲突
    • 事件触发时记录时间戳
    • 设置锁控制 click 事件执行与否
    • mousedown 和 mouseup 触发的时间戳间隔超过某个值(如:300ms),就判断为拖拽
      • 此时 click 事件上锁,不执行
var firstTime = 0,
  lastTime = 0,
  key = false;
document.onmousedown = function () {
  firstTime = new Date().getTime();
};
document.onmouseup = function () {
  lastTime = new Date().getTime();
  if (lastTime - firstTime < 300) {
    key = true;
  }
};
document.onclick = function () {
  if (key) {
    console.log("click");
    key = false;
  }
};

3)键盘事件

  • keydown
    • 键盘按下
    • 可以响应任意键盘按键
  • keyup
    • 键盘抬起
  • keypress
    • 键盘按下
    • 只可以响应字符类键盘按键
    • 返回 ASCII 码,可以转换成相应的字符
      • 可以区分大小写
      • String.fromCharCode(e.charCode) 返回 ASCII 码对应的字符
  • 触发顺序:keydown > keypress > keyup

4)文本操作事件

  • input
    • 输入文本
  • focus
    • 聚焦输入框
  • blur
    • 输入框失焦
  • change
    • 对比的是聚焦和未聚焦时的状态是否改变

5)窗体操作类事件

  • 定义在 window 上
  • scroll
    • 窗体滚动
  • load
    • 文档加载完成
    • 一般用于设置广告

警告

load 事件非必要不使用

正常解析完文档,下载完 JS 文件就可以操作 DOM

而 load 事件需要解析完文档,构建完 DOMTree、CSSTree,异步下载完图片、视频等资源文件后再执行

严重拖慢文档加载速度

(十六)JSON、异步加载、时间线

1.JSON

  • JSON 是一种传输数据的格式
    • 以对象为样板,本质上就是对象
    • 用途有区别,对象是本地使用的,JSON 是传输数据的
  • JSON.parse()
    • string => json
  • JSON.stringfy()
    • json => string
  • JSON 的属性名必须加双引号,对象的属性名可加可不加

2.异步加载

1)重排重绘

  • 解析标签时遵守 深度优先 原则
  • 生成 DOMTree 代表的是文档解析完成,而不是加载完成
    • 图片、视频、文字等资源会有其他线程负责下载
  • DOMTree + CSSTree = RenderTree
  • reflow 重排
    • 会基于新的 DOMTree 重新构建 RenderTree,效率最低
    • 触发操作
      • dom 节点的删除、添加
      • dom 节点的宽高、位置变化
      • display none -> block
      • 使用 offsetWidth、offsetHeight
  • repaint 重绘
    • 会基于新的 CSSTree 重新构建 RenderTree 中 受影响的部分 ,效率低
    • 触发操作
      • 修改 dom 节点字体颜色等
      • 修改 dom 节点背景颜色等

2)JS 加载的缺点

  • 加载工具方法没必要阻塞文档
  • 过多 JS 加载会影响页面效率
  • 一旦网速不好,则整个网站将等待 JS 加载而不进行后续渲染等工作

相关信息

部分工具方法需要按需加载,用到的时候再加载,不用则不加载

3)异步加载 JS 的三种方案

  • defer 异步加载
    • 需要等到 DOM 文档全部解析完才会被执行
    • 只有 IE9 及以下能使用
    • 可以将 JS 代码写到 script 标签内部
<script type="text/javascript" src="tool.js" defer>
  var str = '我可以使用';
</script>
  • async 异步加载
    • 加载完立即执行
    • 只能加载外部脚本
    • 不能把 JS 代码写到 script 标签内部
    • 执行时不阻塞页面
<script type="text/javascript" src="tool.js" async></script>
  • 创建 script,插入到 DOM 中,加载完毕后执行 callback 函数【最常用】
    • 下载完会触发 load 事件,只能在其中使用下载的外部脚本中的变量或方法
      • Chrome、Safari、Firefox、Opera 可以使用
    • IE 只有 script 没有 load 事件,使用的是属性 readyState
      • 会根据下载脚本时的状态动态改变该属性的值
      • 下载时:script.readyState = 'loading';
      • 下载完成时:script.readyState = 'complete';script.readyState = 'loaded';
      • 同时提供 onreadystatechange 事件,监听该属性值的改变
// 创建script,创建完即下载,但是不执行
var script = document.createElement("script");
script.type = "text/javascript";
script.src = "tool.js";

// 使用tool.js中的变量或方法
if (script.readyState) {
  script.onreadystatechange = function () {
    if (script.readyState === "complete" || script.readyState === "loaded") {
      console.log(a);
      test();
    }
  };
} else {
  script.onload = function () {
    console.log(a);
    test();
  };
}

// 挂到DOM树上时才开始执行
document.head.appendChild(script);
// 封装函数异步加载JS脚本
function loadScriptAsync(url, callback) {
  var script = document.createElement("script");
  script.type = "text/javascript";
  if (script.readyState) {
    script.onreadystatechange = function () {
      if (script.readyState === "complete" || script.readyState === "loaded") {
        // callback();
        // eval(callback);
        tools[callback]();
      }
    };
  } else {
    script.onload = function () {
      // callback();
      // eval(callback);
      tools[callback]();
    };
  }
  script.src = url; // 绑定事件后再下载资源,防止资源下载速度过快而事件还没绑定上
  document.head.appendChild(script);
}
  • 方案一:调用该封装函数时,需要把 callback 声明为匿名函数引用,否则 test 未定义报错
  • 方案二:也可以传递字符串,在封装函数内部不直接执行 callback(),而是通过 eval(callback)
  • 方案三:外部脚本定义为一个对象的属性,封装函数内部通过 tools[callback]()
loadScriptAsync("tool.js", function () {
  test();
});

loadScriptAsync("tool.js", "test()");

// var tools = { test: function() {}, demo: function() {} }
loadScriptAsync("tool.js", "test");

3.JS 加载时间线

  • 创建 Document 对象,开始解析 Web 页面
    • 解析 HTML 元素和他们的文本内容后,添加 Element 对象和 Text 节点到文档中
    • 这个阶段 document.readyState = 'loading';
  • 遇到 link 外部 CSS,创建线程加载,并继续解析文档
  • 遇到 script 外部 JS,并且没有设置 async、defer,浏览器加载并阻塞
    • 等待 JS 加载完成并执行该脚本,然后继续解析文档
  • 遇到 script 外部 JS,并且设置了 async、defer,浏览器创建线程加载,并继续解析文档
    • 对于 async 属性的脚本,脚本加载完成后立即执行
    • 异步代码禁止使用 document.write();
      • 因为会消除在此之前文档流中的所有内容
  • 遇到 img、video、audio、iframe 等标签,先正常解析 DOM 结构,然后浏览器异步加载 src,并继续解析文档
  • 当文档解析完成,document.readyState = 'interactive';
  • 文档解析完成后,所有设置有 defer 的脚本会按照顺序执行
    • 与 async 不同,但同样禁止使用 document.write();
  • document 对象触发 DOMContentLoaded 事件,这也标志着程序执行从 同步脚本执行阶段 转化为 事件驱动阶段
    • 该事件只能通过 addEventListener 绑定
    • JQuery 库的一个方法 $(document)ready(function(){}); 就是监听该事件
    • window.onload 区别:文档解析完成后再执行(更好),load 是文档资源全部加载完成后再执行
  • 当所有 async 脚本加载完成并执行、img 等资源加载完成后,document.readyState = 'complete';
    • window 对象触发 load 事件
  • 从此,以异步响应方式处理用户输入、网络事件等
console.log(document.readyState);
document.onreadystatechange = function () {
  console.log(document.readyState);
};
document.addEventListener(
  "DOMContentLoaded",
  function () {
    console.log("a");
  },
  false,
);
// loading
// interactive
// a
// complete

(十七)BOM

  • 由于浏览器厂商不同,BOM 对象的兼容性极低
  • 一般情况下,只用其中的部分功能

1.定义

  • Browser Object Model
  • 定义了操作浏览器的接口

2.BOM 对象

  • Window
  • History
  • Navigator
  • Screen
  • Location

1)Location 对象

  • location.hash
  • 浏览器地址的 # 后面的部分是对浏览器操作的
    • 对服务器无效
    • 实际发出的请求也不包含 # 之后的部分
  • # 被算作历史记录

(十八)正则表达式

1.补充知识

1)转义字符 \

  • \t
    • 制表符
  • var str = "abcd\"efg";
  • var str = "abcd\\efg";

2)多行字符串

  • 将回车符转义为普通换行符,控制台就不会报错
document.body.innerHTML =
  "\
  <div></div>\
  <span></span>\
";

3)字符串换行符 \n

  • \n
    • 换行
  • \r
    • 行结束
  • \r\n
    • 部分操作系统表示一个换行

2.正则表达式 RegExp

1)作用

2)两种创建方式

  • 直接量【推荐】
var reg = /abc/;
var reg = /abc/i;
  • new RegExp()
var reg = new RegExp("abc");
var reg = new RegExp("abc", "i");
var reg = /abc/m;

// 正常情况,创建了两个规则对象
var reg1 = new RegExp(reg);
console.log(reg); // /abc/m
console.log(reg1); // /abc/m
reg.abc = 123;
console.log(reg1.abc); // undefined

// 非正常情况(没有new),实际上是同一个规则对象
var reg1 = RegExp(reg);
console.log(reg); // /abc/m
console.log(reg1); // /abc/m
reg.abc = 123;
console.log(reg1.abc); // 123

3)修饰符

修饰符说明
iignoreCase,忽略大小写
gglobal,全局匹配(匹配多个)
mmultiline,多行匹配(搭配限制开头结尾的规则)
var reg = /^a/g; // 全局匹配以a开头的字符串
var str = "abcdea";
console.log(str.match(reg)); // ["a"]

var reg = /^a/gm; // 全局且多行匹配以a开头的字符串
var str = "abcde\na";
console.log(str.match(reg)); // ["a", "a"]

4)Unicode 编码

  • \u[HHHH] 四位十六进制
  • \u[NO][HHHH] 两位表示第几层,四位十六进制表示编码
    • 第一层:\u010000 - \u01ffff
    • 第二层:\u020000 - \u02ffff
    • ...
    • 第十六层:\u100000 - \u10ffff

5)元字符

元字符含义
\w[0-9A-z_]
\W[^\w]
\d[0-9]
\D[^\d]
\s[\t\n\r\v\f ]
\S[^\s]
\b单词边界
\B非单词边界
.[^\r\n]
// 表示匹配一切字符
var reg = /[\s\S]/g;
// <=>
var reg = /[\d\D]/g;

6)量词

  • 匹配时遵循贪婪原则
  • 能匹配多个就尽可能匹配多个
量词含义
n+{1, }
n*{0, }
n?{0, 1}
n{x}{x}
n{x,y}{x, y}
n{x, }{x, } 不写就是 Infinity
var reg = /\w*/g;
var str = "abc";
console.log(str.match(reg)); // ["abc", ""]
// 匹配到abc时,识别到g且*允许匹配0个,所以又多匹配了一个空串

7)开头结尾

  • 两者一起使用能起到限制字符串的作用
  • 结尾$受多行匹配 m 的影响
var reg = /^abc$/g;
var str = "abcabc";
console.log(str.match(reg)); // null
// 实际上匹配的就是"abc"

8)面试题

  • 检验一个字符串首尾是否含有数字
    • 理解:首或尾有数字就匹配,因为题目没说“都含有”
var reg = /^\d|\d$/g;
var str = "123abc";
console.log(str.match(reg)); // "1"

3.RegExp 对象方法

1)reg.exec()

  • 当且仅当 g 全局匹配时,有以下规则
    • 每次匹配游标递增
    • 直到匹配完字符串会返回 null
    • 再继续匹配则游标返回字符串起始位置
  • 游标即 lastIndex 属性
    • 专为 exec 方法创建
    • 可以手动控制该属性,从而修改 exec 匹配位置
var reg = /ab/g;
var str = "abababab";
console.log(reg.lastIndex); // 0
console.log(reg.exec(str)); // [..., index: 0, ...]
console.log(reg.exec(str)); // [..., index: 2, ...]
console.log(reg.exec(str)); // [..., index: 4, ...]
console.log(reg.exec(str)); // [..., index: 6, ...]
console.log(reg.exec(str)); // null
console.log(reg.exec(str)); // [..., index: 0, ...]

2)反向引用子表达式

  • () 可以表示子表达式
  • \1 表示反向引用
    • 反向引用第一个子表达式中匹配到的内容
    • 加多个则反向引用多次
var reg = /(a)\1/g;
var str = "aaaa";
console.log(str.match(reg)); // ["aa"]

var reg2 = /(\w)\1\1\1/g;
var str2 = "aaaabbbb";
console.log(str2.match(reg2)); // ["aaaa", "bbbb"]
var str3 = "aabb";
console.log(str3.match(reg2)); // null
var reg3 = /(\w)\1(\w)\2/g;
console.log(str3.match(reg3)); // ["aabb"]

警告

  • exec 搭配反向引用
    • 结果集的类数组中会增加子表达式的匹配结果
    • 不受 g 限制,加不加全局匹配都可行
var reg = /(\w)\1(\w)\2/g;
var str = "aabb";
console.log(reg.exec(str)); // ["aabb", "a", "b", index: 0, input: "aabb"]

3)match()

  • 是 String 的方法
  • 搭配反向引用
    • 结果集的类数组中会增加子表达式的匹配结果
    • 受 g 限制,加了全局匹配就失效
var reg = /(\w)\1(\w)\2/;
var str = "aabb";
console.log(str.match(reg)); // ["aabb", "a", "b", index: 0, input: "aabb"]

var reg2 = /(\w)\1(\w)\2/g;
console.log(str.match(reg2)); // ["aabb"]

4)replace()

  • 是 String 的方法
  • 没有 全局匹配的功能
    • 需要搭配正则表达式
  • 替换的字符串
    • 可以使用 $ 来使用反向引用匹配到的子表达式的值
      • 如果需要把匹配结果替换为 $ 需要使用 $$,相当于转义字符
    • 也可以使用回调函数
      • 参数 1:正则表达式匹配的结果
      • 参数 2:第一个子表达式匹配的结果
      • 参数 3:第二个子表达式匹配的结果
var str = "aa";
console.log(str.replace("a", "b")); // ["ba"]

var reg = /a/g;
var str = "aa";
console.log(str.replace(reg, "b")); // ["bb"]

var reg = /(\w)\1(\w)\2/g;
var str = "aabb";
console.log(str.replace(reg, "$2$2$1$1")); // ["bbaa"]

var reg = /(\w)\1(\w)\2/g;
var str = "aabb";
console.log(
  str.replace(reg, function ($, $1, $2) {
    return $2 + $2 + $1 + $1;
  }),
); // ["bbaa"]
  • 把 "the-first-name" 替换为 "theFirstName"
var reg = /-(\w)/g;
var str = "the-first-name";
console.log(
  str.replace(reg, function ($, $1) {
    return $1.toUpperCase();
  }),
);

5)正向预查/正向断言

  • 选出一个 a,且该 a 后面紧跟着 b
  • b 不参与选择,只作修饰
var reg = /a(?=b)/g;
var str = "abaaaaa";
console.log(str.match(reg)); // ["a"]

6)非贪婪匹配

  • 匹配尽量少的字符
  • ??
    • 第一个代表量词,取{0, 1}个
    • 第二个代表非贪婪,能取 0 就不取 1
  • *?
    • 第一个代表量词,取{0, }个
    • 第二个代表非贪婪,能取 0 就不取多
var reg = /a+/g;
var str = "aaaaaa";
console.log(str.match(reg)); // ["aaaaaa"]

var reg2 = /a+?/g;
console.log(str.match(reg2)); // ["a", "a", "a", "a", "a", "a"]

7)面试题

  • 将字符串转换为每三位用 "." 分隔开的写法
  • "10000000000" => "10.000.000.000"
    • 先把后面位数是 3 的倍数的空字符匹配出来
var str = "10000000000";

// var reg = /(\d(?=\d{3}))\1$/g;
// console.log(reg.replace(str, ". $1")); // ✖

var reg = /(?=(\d{3})+$)/g;
console.log(reg.replace(str, "."));
// 不足:"100000000000" => ".100.000.000.000"

var reg = /(?=(\B)(\d{3})+$)/g;
console.log(reg.replace(str, ".")); // "100.000.000.000"
上次编辑于: