三、Web前端开发JavaScript高薪课堂
(一)JavaScript 入门
1.Web 发展史
- Mosaic,是互联网历史上第一个获普遍使用和能够显示图片的 网页浏览器 ,于 1993 年问世
2.浏览器组成
- shell 部分
- 内核部分
- 渲染引擎(语法规则和渲染)
- JS 引擎
- 其他模块
3.翻译过程
1)编译
- 通篇翻译
- 将格式 1 的文件翻译成格式 2 的文件,由系统程序执行格式 2 文件
- 该类型的编程语言叫编译性语言
- 优点:过程快
- 缺点:翻译后的格式 2 文件移植性不好(不能跨平台)
- 如:
- C 语言:
.c => .obj
- C++
- C 语言:
2)解释
- 逐行翻译,逐行执行
- 直接翻译格式 1 的文件,翻译一行就交给系统程序执行一行
- 该类型的编程语言叫解释性语言
- 优点:直接翻译成机器码,可以跨平台
- 缺点:过程稍微慢一点
- 如:
- JavaScript
- PHP
- Python
3)Java
- Java 既不是编译性语言,也不是解释性语言,是 oak 语言
.java
文件通过javac
命令编译成.class
文件,再经过jvm
虚拟机解释执行- 可以跨平台
4.JS 特点
- 解释性语言
- 不需要编译成文件,可以跨平台
- 单线程
- ECMA 标注
- 兼容于 ECMA 标准,因此也称为 ECMAScript
- 三大部分:
- ECMAScript(ES)
- DOM
- BOM
5.主流浏览器
浏览器 | 内核 |
---|---|
IE | Trident -> Edge |
Firefox | Gecko |
Chrome | Webkit -> Blink |
Safari | Webkit |
Opera | Presto -> 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;
- 关键字
1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|
break | else | new | var | case | finally | return |
void | catch | for | switch | while | default | if |
throw | delete | in | try | do | instanceof | typeof |
- 保留字
1 | 2 | 3 | 4 |
---|---|---|---|
abstract | enum | int | short |
boolean | export | interface | static |
byte | extends | long | super |
char | final | native | synchronized |
class | float | package | throws |
const | goto | private | transient |
debugger | implements | protected | volatile |
double | import | public |
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
- 有些浏览器会忽略 radix,当成
- 关注点在于转成整型
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;
- eg:
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;
}
(四)作用域、作用域链
[[scope]]
1.作用域 - 每个 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.立即执行函数
- 此类函数没有声明,在一次执行过后就释放
- 适合做初始化工作
- 除了初始化页面,后续需要用到的数据都需要被返回
(function() {}())
1)形式一:- 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;
})();
(function() {})()
2)形式二:- 只有表达式才能被执行符号执行:
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 就只能返回对象
- 在构造函数顶部隐式创建 this 的对象(仅包含
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
(七)原型、原型链
prototype
1.原型 - 原型是 function 的一个属性,它定义了构造函数制造出的对象的公共祖先
- 通过该构造函数产生的对象,可以继承该原型的属性和方法
- 原型也是对象,默认为空对象
- 利用原型特点和概念,可以提取共有属性
- 对象查看原型:隐式属性
obj.__proto__
- 对象查看构造函数:
obj.constructor
// Person.prototype 原型
// Person.prototype = {}; 是祖先
Person.prototype.lastName = "Deng";
function Person() {}
__proto__
2.隐式属性 - 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 有
- 绝大多数 对象的最终都会继承自
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.对象的枚举
for...in..
1)- 通过对象的属性个数来控制循环次数
- 会沿着原型链遍历所有属性
- 一旦遍历到最顶层原型(
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);
}
hasOwnProperty(prop)
2)- 判断属性是不是自身的
- 是则返回 true
if (obj.hasOwnProperty("name")) {
console.log("这个name属性是obj自己的,不是继承来的");
}
prop in obj
3)- 判断 obj 能不能访问到 prop
- 包括原型链上可访问的属性
- 能则返回 true
console.log("name" in obj);
A instanceof B
4)- 判断 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 指向
obj.func();
的 func() 中的 this 指向 obj
4.- 谁调用函数,函数中的 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 时,参数不动
- 执行 sort 时,依次从数组中取出两位,分别传入处理函数作为实参,返回值即当前两位数的排序结果
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 的语法
- 如果开启严格模式,那么使用 ES5 的语法
2)严格模式
- 本质是一行字符串,不会对不兼容严格模式的浏览器产生影响
- 不支持
with
、arguments.callee
、func.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 |
document | 9 |
DocumentFragment | 11 |
<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 浏览器没有
- 选中的元素不是实时的,选出来的其实是元素的副本【镜像】
- 选完后,元素新增删除,结果集都不变化
- CSS 选择器
document.querySelectorAll()
- CSS 选择器
"div > p .demo"
- 选一组
- IE7 及以下版本的 IE 浏览器没有
- 选中的元素不是实时的,选出来的其实是元素的副本【镜像】
- 选完后,元素新增删除,结果集都不变化
- CSS 选择器
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>
标签
- body、head 分别指代 HTML 文档中的
- Document.prototype 定义了 documentElement 属性
- 指代文档的根元素
- 在 HTML 文档中,它总是指代
<html>
元素
getElementsByClassName
、querySelector
、querySelectorAll
在 Document.prototype、Element.prototype 类中均有定义
// 选择所有标签
document.getElementsByTagName("*");
(十三)Date 对象、定时器
1.日期对象 Date()
- 系统封装好的构造函数
- 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.scrollLeft
、document.body.scrollTop
document.documentElement.scrollLeft
、document.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.innerWidth
、window.innerHeight
- IE8 及以下不兼容
document.documentElement.clientWidth
、document.documentElement.clientHeight
- 标准模式下,任意浏览器都兼容
- document.compatMode = 'CSS1Compat'
document.body.clientWidth
、document.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.offsetLeft
、dom.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.绑定事件处理函数
ele.onxxx = function (event) {}
1)- 兼容性很好
- 一个元素的同一个事件上只能绑定一个处理函数
- 基本等同于直接写在 HTML 行间上
obj.addEventListener(type, fn, false)
2)- IE9 以下不兼容
- 可以为一个事件绑定多个处理函数
obj.attachEvent('on' + type, fn)
3)- 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.事件处理程序的运行环境
ele.onxxx = function (event) {}
1)- 程序 this 指向的是 dom 元素本身
obj.addEventListener(type, fn, false)
2)- 程序 this 指向的是 dom 元素本身
obj.attachEvent('on' + type, fn)
3)- 程序 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 两者都有
- Firefox 仅有
- 兼容性写法:
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 事件,监听该属性值的改变
- 下载完会触发 load 事件,只能在其中使用下载的外部脚本中的变量或方法
// 创建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();
- 与 async 不同,但同样禁止使用
- 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>\
";
\n
3)字符串换行符 \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)修饰符
修饰符 | 说明 |
---|---|
i | ignoreCase,忽略大小写 |
g | global,全局匹配(匹配多个) |
m | multiline,多行匹配(搭配限制开头结尾的规则) |
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"