JavaScript yyds
JavaScript历史
https://www.ruanyifeng.com/blog/2011/06/birth_of_javascript.html
JavaScript作为Netscape Navigator浏览器的一部分首次出现在1996年。它最初的设计目标是改善网页的用户体验。
简单来说JavaScript分为三个部分:
- ECMAScript
- DOM
- BOM
ECMAScript 就是作者 Brendan Eich 创办的,DOM和BOM也是,但不是由他完全创建的,作者给出了标准,由各个浏览器厂商给出具体的实现。这三部分结合就是一个全新的 JavaScript。
版本问题:ES1.0废弃了,ES2.0没有用上,而我们现在使用的JavaScript都是基于ES3.0的。
3.0是最基础,最全面,最强大的版本,5.0是在3.0的基础上加上了一些方法而已,还并不是所有浏览器都支持。6.0是在3.0和5.0的基础上又添加了一些新方法。
本篇博客内容涉及的都是 ES3.0和 ES5.0的内容。
主流浏览器及其内核
浏览器 | 内核 |
---|---|
IE | Trident |
Chrome | Webkit —> Blink |
FireFox | Gecko |
Opera | Presto —> Blink |
Safari | Webkit |
JavaScript中的数据类型
大体可分为两种:
- 不可改变的原始值(栈数据)
- Number
- String
- Boolean
- undefined
- null
- 引用值(堆数据)
- array
- object
- function
- …
typeof函数
js 中typeof函数返回的结果有6种情况:
number string boolean object undefined function注意:数组和null的
typeof
也是objecttypeof有个极特殊情况,js中变量未定义而进行使用,只有一种情况不发生报错,那就是把变量放入typeof函数里。
特殊情况
数字1可以加减字符串"1":
var num = 1 + "1"; //11
var num = 1 - "1"; //0
var num = "2" - "1"; //1
var num = "2" * "1"; //2
为什么可以运算出结果?原因是JavaScript中隐式类型转换。
而隐式类型转换都是基于显示类型转换的。
显示类型转换
-
Number(val)
Number() 函数旨在将任意类型,转换为数字类型,如果不能转成整型或浮点型,则转换为NaN。
true转为数字为 1,false转为数字为 0
null转为数字为 0
undefined转为数字为 NaN
所有经Number函数转换的值,最终都会变成数字类型,包括NaN也属于数字类型。
-
parseInt(val,radix)
parseInt()函数旨在将字符串或数字转化为10进制的整型,它不会关注bolean值、null和undefined,那些值放入parseInt()中全部被转化为NaN,如果是浮点数,则直接砍掉小数位。
parseInt会从第一位开始记录,一直到非数字位截止,例如:parseInt(“123px14”)转化的结果为 123.
参数radix,表示val值的进制。parseInt转换的目标是10进制,而参数val的进制,可由第二个参数radix指定。即radix进制=>10进制。radix的取值范围: [0] [2 , 36],一进制是不存在的。如果写0进制,则把val原封不动地返回。
如果转换失败,返回NaN,例如 parseInt(2,2)
-
parseFloat(val)
parseFloat()会识别从第一位到第一个点结尾后最后一个的非数字位结束的值,它不涉及进制转换。例如:
console.log(parseFloat("12.3.4fsf")) //12.3 console.log(parseFloat("a12.3.4fsf")) //NaN
-
toString(radix)
toString()方法与其他几个方法不同,它并非属于全局方法,而是需要以对象点的形式来调用。注意:undefined和null值不能调用toString方法。
其中,10进制是源进制,radix是目标进制。即10进制=>radix进制
举个例子,把“10101”这个二进制字符串,转换为16进制的:
/** * 二进制转十六进制 * 先转成10进制,再转成16进制 */ var target = "10101"; var num = parseInt(target,2); console.log(num) //21 num = num.toString(16); console.log(num) //15
-
String(val)
String()函数试图把任意类型转化为字符串
-
Boolean(val)
Boolean()函数试图把任意类型转化为boolean值,除0、NaN、null、undefined、“”(空字符串)被转换为false外,所有其他值都被转换为true。
隐式类型转换
-
++ / – (自增 / 自减) + / - (正 / 负)
这些运算符的背后隐式调用了Number()
为什么+new Date() == (new Date).getTime(); 因为+new Date相当于隐式调用了Number(new Date()) -
+
+运算符的背后隐式调用了String()
加号的两侧只要有一个值是字符串,另一侧的值会调用String()隐式转化,得出的结果就是字符串。
如果加号的两侧没有字符串,则加号两边的值会通过Number()做隐式类型转换。
-
- * / %
这些运算符两侧的字符串会通过Number()做隐式类型转换。例如var num = “123” * “2”; // 246
-
&& || !
其中隐含着Boolean()的转换
-
< > <= >=
比较运算符 两边如果有数字,则都转换为数字进行比较,如果两边都是字符串,则比较的是ASCII码值。
-
== !=
Boolean()转换,Number()转换
1 == true 1 == “1”
-
isNaN()
isNaN()函数的底层,实际是调用了Number()来进行隐式类型转换。
但是,它可不是return NaN === Number(val)
因为在 js 中 NaN == NaN(false)
所以我们猜想 isNaN()函数的实现是:return Object.is(NaN, Number(val))
特殊情况:
undefined和null都不等于,不大于,不小于任何值,但是:undefined == null (true) 、undefined === null (false)
NaN也不等于,不大于,不小于任何值,甚至连它本身都不等于:NaN == NaN(false)
不发生类型转换的:
- === !=== 绝对等于和绝对不等于
类型转换的小案例
var str = false + 1;
console.log(str);
var demo = false == 1;
console.log(demo);
if (typeof(a)&&-true + (+undefined) + ""){
console.log("基础扎实");
}
if (11 + "11" * 2 == 33){
console.log('基础扎实');
}
!!" " + !!"" - !!true||console.log('你觉得能打印,你就是猪');
执行结果
1
false
基础扎实
基础扎实
你觉得能打印,你就是猪
函数
命名函数声明
function func1(){
}
匿名函数表达式
var func2 = function abc(){
}
var func3 = function(){
}
以上例子中,func1是比较标准的函数定义方式,而func2和func3用一个变量接收函数体,变量名充当了函数名,像func3这种匿名函数表达式的形式也很常用。当然它们使用起来几乎没有太大差别。
它们之间的小区别:
console.log(func1,func2,func3); //不报错
console.log(abc); //报错:Uncaught ReferenceError: abc is not defined
console.log(func1.name); //func1
console.log(func2.name); //abc
console.log(func3.name); //func3
此外,命名函数声明和匿名函数表达式的预编译的结果是不同的。
实参列表—arguments
当实参个数大于形参个数时,我们还能否取到那些余下的实参?
function sum(a) {
console.log(a);
}
sum(1,2,3); // 1
答案是可以的,js中每个函数内部都封装了实参列表对象,它类似于数组的结构:
function sum(a) {
console.log(arguments);
console.log(arguments.length);
}
sum(1,2,3);
但是arguments对象不是数组:
Array.isArray(arguments)
的结果为false
获取实参的个数知道了,那如何获取形参的个数呢?方法名.length就是形参个数
function sum(a,b,c,d,e) {
console.log(sum.length) //5
}
sum();
不定长参数:
function sum1(...a) {
console.log(a)
console.log(Array.isArray(a)) //true
}
function sum2() {
console.log(arguments)
console.log(Array.isArray(arguments)) //false
}
sum1(1,2,3);
sum2(1,2,3);
arguments和形参的双向数据绑定:
function sum(a,b,c) {
console.log(a); //1
console.log(arguments); // [1,2,3]
a = 100;
console.log(arguments); // [100,2,3]
arguments[1] = 301;
console.log(b); //301
}
sum(1,2,3);
英语小词
event 事件,大事
case 情况,案例(小事)
另外,JavaScript中函数不存在重载,重名的函数 后者覆盖前者。
但是有没有函数重写一说,这个可以有,原型链里面会介绍到。
js运行三部曲
- 语法分析
- 预编译
- 解释执行
预编译小口诀:
函数声明整体提升
变量 声明提升
然而这两句口诀仅仅是预编译的结果或现象,并不能解决所有预编译相关的问题
预编译前奏
- imply global 暗示全局变量:即任何变量,如果变量未经声明就赋值,此变量就为全局对象所有。
- eg:a = 123;
- eg: var a = b = 234;
- 一切声明的全局变量,全是window的属性
- eg:var a = 123; ===> window.a = 123;
预编译的过程发生在函数执行的前一刻。
预编译-四部曲
函数的预编译:
- 创建AO对象(Activation Object ,执行期上下文)
- 找形参和变量声明,将变量和形参名作为AO对象的属性名,值为undefined
- 将实参的值和形参统一
- 在函数体里面找函数声明,值赋予函数体
拓展:预编译的过程中,arguments对象会放到AO,this指向也会放到AO,this默认指向全局GO
全局的预编译:
- 创建GO对象(Global Object)
- 找变量声明,将变量名作为GO对象的属性名,值为undefined
- 在全局找函数声明,值赋予函数体
注意:window 其实就是 GO对象
全局预编译是早于函数预编译的,js代码如果不执行,不会发生全局预编译,同样,函数如果没有调用,也不会发生函数预编译。
预编译的目的,是为了js代码可以更好地解释执行,并解决js中变量重复声明以及变量和方法重名等问题。可以说,预编译为js代码重新定义了执行顺序,其主要指的是变量定义和函数声明的顺序。
案例一
function fn(a){
console.log(a);
var a = 123;
console.log(a);
function a() {}
console.log(a);
var b = function () {}
console.log(b);
function d() {}
}
fn(1);
以上代码的执行结果是什么?
首先进行全局预编译:
(第一步) GO{ } (第二步) GO{ //无变量声明 } (第三步) GO{ fn : function fn() {//...} }
然后,函数fn()被调用:fn(1); 在函数执行的前一刻,进行函数预编译:
(第一步) AO{ } (第二步) AO{ a : undefined, b : undefined } (第三步) AO{ a : 1, b : undefined } (第四步) AO{ a : function a() {}, b : undefined, d : function d() {} }
最后,fn()函数内部的代码被一行一行地解释执行:
function fn(a){ console.log(a); // 打印出a,此时的结果为 function a() {} var a = 123; /** * var a;可以忽略,它在预编译的时候处理过了,此时,只执行赋值语句: * a = 123; * 此时AO对象中的a也相应更改: * AO{ * a : 123, * b : undefined, * d : function d() {} * } */ console.log(a); // 打印结果为 123 function a() {} /** * function a(){} 这个函数声明可以忽略,它在预编译的时候处理过了 */ console.log(a); // 此时的a还是等于 123 var b = function () {} /** * 这是一个匿名函数表达式,预编译时候没有处理它 * 因此,此时的AO对象为: * AO{ * a : 123, * b : function b() {}, * d : function d() {} * } */ console.log(b); // 打印的结果是: function b() {} function d() {} //忽略该函数声明 }
最后,打印出来的结果应该为:
function a() {} 123 123 function b() {}
事实证明了我们的猜想:
[Function: a] 123 123 [Function: b]
案例二
function test(a,b) {
console.log(a);
c = 0;
var c;
a = 3;
b = 2;
console.log(b);
function b() {}
function d() {}
console.log(b);
}
test(1);
执行结果
1
2
2
案例三
console.log(test);
function test(test) {
console.log(test);
var test = 234;
console.log(test);
function test() {}
}
test(1);
var test = 123;
执行结果
[Function: test]
[Function: test]
234
案例四
global = 100;
function fn() {
console.log(global)
global = 200;
console.log(global)
var global = 300;
}
fn();
var global;
执行结果
undefined
200
案例五
function test() {
console.log(b);
if (a) {
var b = 100;
}
c = 234;
console.log(c);
}
var a;
test();
a = 10;
console.log(c);
执行结果
undefined
234
234
案例六
function bar() {
return foo;
foo = 10;
function foo() {}
var foo = 11;
}
console.log(bar())
执行结果
[Function: foo]
案例七
console.log(bar())
function bar() {
foo = 10;
function foo() {}
var foo = 11;
return foo;
}
执行结果
11
案例八
a = 100;
function demo(e) {
function e() {}
arguments[0] = 2;
console.log(e);
if (a) {
var b = 123;
function c() {
}
}
var c;
a = 10;
var a;
console.log(b);
f = 123;
console.log(c);
console.log(a);
}
var a;
demo(1);
console.log(a);
console.log(f);
执行结果
2
undefined
undefined
10
100
123
立即执行函数
我们知道,每个被定义的函数都有一个作用域链,定义的函数多了,也会占用一定的内存。
有些函数定义之后,只调用了一次,之后保证再也不会使用了,那这样的功能函数,可以写为立即执行函数,有利于节省内存空间。例如初始化函数。
立即执行函数的写法是这样的:
(function test() {
//...
}())
这是js提供的唯一可以立即销毁函数的方式,其他函数都不能手动销毁。
立即执行函数除了可以立即执行,执行完以后销毁的特性外,与普通函数几乎没有区别。它同样具有预编译、作用域链等特性。
立即执行函数可以具有参数和返回值:
var result = (function factorial(n) {
if (n == 1){
return 1;
} else {
return factorial(n-1) * n;
}
}(10))
console.log(result)
且立即执行函数可以不用定义函数名,写成匿名函数的形式:(递归形式除外)
var result = (function(...n) {
var result = 0;
for (var i = 0; i < n.length; i++) {
result += n[i];
}
return result;
}(1,2,3,4))
console.log(result)
立即执行函数并不是JavaScript的设计者专门设计出来的,而是JavaScript的使用者后来发现的。
它巧妙的利用了js中函数可被转为表达式,表达式可立即执行的特性。一些特殊的数学符号(+、-、!)等都具有让函数变成表达式的功能,当然小括号也不例外,小括号是运算符中可以让表达式优先计算的特殊符号,再配合函数的执行符号,就构成了js中的立即执行函数。
(function demo(){})()
就像css的设计者开发浮动效果的时候,起初仅仅是为了网页能做成类似于报纸哪种文字环绕图片的排版。
立即执行函数,利用的是小括号的特殊性质。
官方给出了立即执行函数的两种写法:
(function (){
}()); //W3C建议使用第一种
(function (){
})();
只有表达式才能被执行符号执行,小括号可以认为是函数的执行符号。
函数声明和函数表达式是不同的:
//函数声明
function demo1(){
}
//函数表达式
var demo2 = function () {
}
所以:
//函数声明
function demo1(){
}(); //报错,语法检查不通过
//函数表达式
var demo2 = function () {
}(); //可以执行
函数表达式如果写成立即执行的形式,会自动忽略函数名字,被执行符号执行过后,函数引用也会被丢弃:
var demo1 = function () {
console.log("demo1")
}
demo1();
console.log(demo1) //function(){//....}
var demo2 = function () {
console.log("demo2")
}()
console.log(demo2)//undefined
函数表达式的本质:
var test = function () {}
/**
其中var test是变量声明
而函数之所以变成表达式原因在于=赋值的缘故
*/
所以,所谓的函数表达式被立即执行过后,函数引用被丢弃就非常好理解了:
var demo2 = function () {
console.log("demo2")
}()
console.log(demo2)//undefined
//变量demo2接收的其实是函数的返回值,而无返回值的函数,强行接收返回值,其结果就是undefined
//所以让函数返回一个值不就好了
var demo2 = function () {
console.log("demo2")
return true;
}()
console.log(demo2)//true
那么,函数的引用 自然而言也可以返回出来:
var test = function test() {
console.log("hello");
return test;
}()
test();
test();
test();
//既能立即执行,还能后期不断调用
其他的,最典型的,+(正)-(负)!(非)
这些符号加到函数前面可以使得声明函数变成函数表达式,再配合执行符号能够让其立即执行:
+function test() {
console.log(test);//function(){...}
}()
console.log(test) //报错 Uncaught ReferenceError: test is not defined
其他符号(&&、||)也可以做到,只不过它前面得写一个表达式。
下面写一个练习题:
function test(a,b,c,d){
console.log(a+b+c+d)
}(1,2,3,4);
问以上代码的执行结果?
这道题是一个大坑,许多人会往里面跳。
不了解js的,得出的答案也许是
10
了解立即执行函数的,得出的答案也许是
代码报语法错误
殊不知,这里的答案是:代码可以执行,不会报错,也不会打印任何值。
为什么呢?
js解释器在解释块代码的时候,把它分成了两块:
//这是一块
function test(a,b,c,d){
console.log(a+b+c+d)
}
//这是另一块
(1,2,3,4);
test(a,b,c,d)函数被定义,这无可厚非。
然后,(1,2,3,4);这串表达式在js里面是允许的,而它的返回值,是4.
var v = (1,2,3,4)
console.log(v) //4
逗号在js中算作一种运算符,配合()使用,例如:
var result = (express1,express2);
先计算表达式1的结果,然后计算表达式2的结果,最后返回表达式2的结果
var a,b,a = b = 100; var num = (a*=2,++b); console.log(num) //101 console.log(a) //200
再来一个练习题:
var x = 1;
if (function f(){}) {
x += typeof f;
}
console.log(x)
执行结果是:1undefined
因为:
function f(){} 一旦被当作if条件,它被括号扩了起来,然后就成为了表达式,函数一旦被当作表达式,它自身的引用就被丢弃, f 本身就成了undefined,在if()条件里,function f(){}是个函数,隐式类型转换为true,条件满足,执行,type undefined不会报错,返回结果是字符串的undefined,1加上字符串的undefined,字符串拼接,最终打印出1undefined。
作用域、作用域链
[[scope]]:每个JavaScript函数都是一个对象,对象中有些属性我们可以访问,比如name、prototype等,但有些不可以,这些隐式属性仅供JavaScript引擎存取,[[scope]]就是其中一个。
[[scope]]指的就是我们所说的作用域,其中存储了 运行期上下文 的集合。
作用域链:[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。
运行期上下文:当函数执行时,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,它所产生的执行上下文被销毁。
查找变量:从作用域链的顶端依次向下查找。
function a() {
function b() {
var b = 234;
}
var a = 123;
b();
}
var glob = 100;
a();
来看看如下的例子:
function a() {
function b() {
function c() {
}
c();
}
b();
}
a();
以上代码的作用域链大致是这样的:
a defined a.[[scope]] => 0:GO
a doing a.[[scope]] => 0:aAO -> 1:GO
b defined b.[[scope]] => 0:aAO -> 1:GO
b doing b.[[scope]] => 0:bAO -> 1:aAO -> 2:GO
c defined c.[[scope]] => 0:bAO -> 1:aAO -> 2:GO
c doing c.[[scope]] => 0:cAO -> 1:bAO -> 2:aAO -> 3:GO
我们说作用域链是属于函数的,每个函数都有各自的作用域链。函数如果只是定义,并未执行,它的作用域链其实也存在,只不过这个作用域链里面没有它自己的执行期上下文。
未经执行的js函数,如果它定义在全局里,系统只是读取了它的函数名,函数内部代码是不会读取的,只要没有出现括号和引号的未终止,其他的语法错误是可以容忍的。而嵌套在全局函数里面的内层函数,只要它外面的函数不执行,那么它就相当于没有定义。
所以我们说,函数声明和函数定义是不一样的,声明的函数不一定会被定义,例如:
function test{
function demo(){
}
}
以上代码中,test()声明在全局,它没有执行,但是它定义了,只要该函数被定义,它的作用域链就存在。而demo()函数因为test()函数没有执行而没有被定义,所以它此时没有作用域链。没有执行的函数,我们也应该探讨的作用域链。
这里稍稍梳理一下思路:
函数未定义:没有作用域链
函数被定义:有作用域链
函数执行:定义状态下的作用域链的顶端push一个自己的执行期上下文
说白了,作用域链就相当于一个函数的一整套执行期上下文(它自己的、它父级函数的,全局的)。
从以上分析中可以看出,内层函数可以拥有外层函数的一整套作用域链,而外层函数无法访问到它内层函数的执行期上下文。
要注意的是:以上例子中,b函数的作用域链是基于a函数作用域链引用的拷贝,b函数完全可以修改a函数和全局变量。
其次,函数一旦执行完毕,它的执行期上下文就会被销毁,等他下一次执行时,生成的执行期上下文对象不是上一次的执行期上下文,自然跟上一次的作用域链也不相同。
一个小例题:
function a() {
function b(val) {
var bbb = 234;
glob += val;
console.log(aaa);
}
var aaa = 123;
return b;
}
var glob = 100;
var demo = a();
demo(2);
console.log(glob)
问:以上代码的执行的结果是多少?
123
102
分析以上代码的执行逻辑:
首先,a被定义,它的作用域链为:GO,然后a函数执行,它的作用域链为:aAO -> GO,a的执行产生了b的定义,此时b作用域链与a的作用域链相同
,然后,返回b函数,a函数执行结束,a作用域链被砍断,此时a的作用域链回归到定义状态,(这里要注意,a的作用域链被砍断的前一刻,b函数被返回了,b函数是拥有那个作用域链的,那个作用域链里包含了a函数即将被销毁的执行期上下文,因为b函数还拥有a函数执行期上下文的引用,所以a函数的那个执行期上下文对象并没有真的被销毁)
在全局里,用一个变量demo存储b函数,即demo() 相当于b() ,demo()定义状态下的作用域链和执行期a作用域链相同,然后demo()函数执行,它执行期间能访问到glob变量和aaa变量也就不足为奇了。 这个过程就叫做 闭包。
但凡是内部的函数,被保存到了外部,这种情况一定生成闭包。
闭包
当内部函数被保存到外部时,将会生成闭包。闭包会导致原有的作用域链不释放,造成内存泄漏。
闭包的作用:
- 实现共有变量
- 可以做缓存(存储结构)
- 可以实现封装,属性私有化
- 模块化开发,防止污染全局变量
一个小题:
function test() {
var arr = [];
for (var i = 0;i < 10 ; i++){
arr[i] = function () {
console.log(i);
}
}
return arr;
}
var myArr = test();
for (var i = 0;i < 10 ;i++){
myArr[i]();
}
大家认为以上代码的执行结果是什么?
它的运行结果是 打印出10个10
看似这个运行结果有点匪夷所思,但其实应该是在意料之中。
首先,js中,for循环里声明的变量,是在函数作用域里,而不是for循环的作用域里。
for (var i = 0;i < 10 ;i++){
//...
}
console.log(i) // 10
再之,js的for循环压根就没有作用域:
for (var i = 0;i < 10 ;i++){
var hello = "hello";
}
console.log(i + hello) // 10hello
其他编程语言我不了解,最起码Java就不是这样。Java里for循环、if语句都是有作用域的。
然后接着分析,根据预编译,for循环里声明的变量 i ,其实是属于整个函数的执行期上下文的。其次,函数被赋值给变量,函数体并非立即执行,而是调用的时候才执行。
在未执行的时候,函数体中的 i 等于多少,没有意义,因为函数如果未执行,系统不会读取函数体内的代码,更不会给里面的变量赋值。而真正执行的时候,那个变量 i 等于多少,取决于它执行期上下文中变量 i 的值,arr里面那10个函数说了不算。
当函数真正执行的时候,那个 变量 i 已经等于10了,所以我们说打印出10个10,也在情理之中,如果打印的不是10个10,那就是意料之外了。
如果想让上面的函数返回的10个函数打印出的结果是0-9,可以这样:
function test() {
var arr = [];
for (var i = 0;i < 10 ; i++){
(function (j) {
arr[j] = function () {
console.log(j);
}
}(i))
}
return arr;
}
var myArr = test();
for (var i = 0;i < 10 ;i++){
myArr[i]();
}
前后两种写法的本质区别是什么呢,是存储到数组里的函数,它们的执行期上下文不一样。
使用闭包机制做一个累加器:
function add() {
var num = 0;
function adder() {
return ++num;
}
return adder;
}
var adder = add();
for (var i = 0;i<100;i++){
var a = adder();
console.log(a)
}
闭包产生的函数,不是纯函数。
使用闭包进行模块化开发的好处:不依赖与外部变量,实现功能的同时,不会出现变量名字冲突。不污染外部变量。
小题:
var f = (
function f() {
return "1";
},
function g() {
return 2;
}
)()
console.log(typeof f)
以上代码的输出结果是什么?
答:number
考察的知识点其实还是(express1,express2),这样的表达式。
对象
js中对象、数组、函数都是引用值类型。
描述一个对象:
var student = {
name : '张三',
age : 40,
sex : 'female',
eat : function () {
console.log("吃东西")
},
hello(){
console.log("hello world")
},
print(){
console.log(this)
}
}
一个对象没有new出来之前,它自己不能访问自己的属性
var obj = { n : 10, m : obj.n * 10 } console.log(obj.m)
报错
Uncaught TypeError: Cannot read property 'n' of undefined
属性的增删改查:
增改查都是 对象. 调用即可,比较简单。
属性的删除:需要使用一个关键字delete
删除sex属性:
delete student.sex
console.log(student.sex) //undefined
//不仅可删除属性,还可删除方法
delete student.print
student.print() //TypeError: student.print is not a function
//如果删除的属性或方法不存在,也不报错
能够删除对象的属性,任意修改对象中方法的实现,这也是JavaScript不同于其他语言的一大特点。
对象的创建方式
-
var obj = { } plainObject 对象字面量、对象直接量
-
构造函数
-
系统自带的构造函数 new Object() Array() Number()
-
自定义的构造函数
构造函数,结构上和js里的普通函数没有任何分别
为了避免和普通函数混淆,约定俗称:函数名首字母大写,遵循大驼峰命名规则
调用构造函数,必须借助关键字:newfunction Car(brand,price,color) { this.brand = brand; this.price = price; this.color = color; this.run = function () { console.log(this.brand+"car is running") } } var car = new Car("比亚迪",1000,"#fff");
-
-
Object.create(原型)
// 通过原型创建对象 var animal = {age:9}; var person = Object.create(animal);
注意:Object.create()方法的参数只能是对象或null,否则就会报错。
构造函数内部原理
一个function如果调用的时候,如果前面使用了new关键字,则会在函数体里面隐式执行一些代码:
例如:new Person(),当代码执行的时候,隐式添加的代码如下:
function Person(name,age,sex) {
/**
* var this = {}
*/
this.name = name;
this.age = age;
this.sex = sex;
/**
* return this;
*/
}
大体上就是这样的,但底层细节可能涉及到的方面更多。
function Person(name,age,sex) {
console.log(this) //Person {}
this.name = name;
console.log(this) //Person { name: 'zs' }
this.age = age;
console.log(this) //Person { name: 'zs', age: 15 }
this.sex = sex;
console.log(this) //Person { name: 'zs', age: 15, sex: 'male' }
}
var p = new Person("zs",15,"male");
关于js构造函数的冷门知识:
虽然在new对象的时候,函数体内隐式地return this;了,但是我们可以修改返回的结果,我们可以显式地return this;,或者return {};
但需注意:返回的值必须是引用值类型,否则不起作用
function Person(name,age,sex) { this.name = name; this.age = age; this.sex = sex; return [11,12,13]; } var person = new Person("hh",18,"f"); console.log(person);//[ 11, 12, 13 ]
function Person(name,age,sex) { this.name = name; this.age = age; this.sex = sex; return "hello world"; } var person = new Person("hh",18,"f"); console.log(person);//Person { name: 'hh', age: 18, sex: 'f' }
开发规范:
var obj = {};
var arr = [];
var obj = new Object(); //不推荐
var arr = new Array(); //不推荐
要使用字面量的形式创建对象和数组,一般情况下一个JavaScript的使用者没有任何理由会使用new的方式去创建对象或数组,即麻烦又没有太大用而且不灵活。
包装类
在JavaScript里,数字,字符串,布尔值 都有其对应的原始值和包装类。
js中的原始值是不能有属性和方法的,而操作包装类的对象,就像操作普通对象,给对象添加属性,方法都可以。
包装类隐式包装的过程:
只要原始值类型的数字,字符串,布尔值 通过打点调用的方式做一系列操作,都会被自动地临时转换为包装类,执行结束销毁包装类:
var num = 10;
num.len = 3; //-->new Number(num).len = 3 新建的这个Number对象因为没有引用指向它,被销毁
console.log(num.len) //-->new Number(num).len 打印出 undefined
var str = "hello";
str.length = 2; //--> new String(str).length = 2 这个String对象也没有引用指向它,被销毁
console.log(str.length) //-->new String(str).length 打印出5
原型、原型链
定义:原型是function对象的一个属性,它定义了构造函数制造出的对象的公共祖先。通过该构造函数产生了对象,可以继承该原型的属性和方法。原型也是对象。
原型使得JavaScript中对象可以具有继承性。
获取一个对象的原型,可以使用 构造方法名.prototype
的形式获得:
function Person() {
}
console.log(Person.prototype)
对象和它自身的原型如果存在相同的属性或方法,优先使用对象自己的。
利用原型的特点和概念,可以提取共有属性。
另外,每一个对象,都具有一个隐式属性:__proto__
隐式属性__proto__
的作用:用来把对象和它的原型关联起来。
我们思考一件事哈,一个JavaScript对象,访问它的name属性,如果这个对象本身没有name属性,是不是会到它的原型prototype里面找,而对象的并没有prototype属性呀,谁给它们关联起来的呢?就是对象的隐式属性:
__proto__
一个对象在new的时候,其构造函数的第一行,是不是会隐式地执行 var this = {},我们必须清楚一点,这个this其实并不是一个空对象:
function Person(name,age,sex) {
/** this不是空对象,它里面是有属性的
* var this = {
__proto__ : Person.prototype
}
*/
this.name = name;
this.age = age;
this.sex = sex;
/**
* return this;
*/
}
然而退一万步讲,什么是空对象,{} 算是空对象了吧,它里面没有任何我们自定义的属性和方法。
然而:
var obj = {}; console.log(obj.__proto__);
可以看到一个空对象,本身就有
__proto__
属性。
而上面我们之所以说,构造函数中的this不是空对象,是因为那个this中
__proto__
指向的是 函数的prototype,而空对象的__proto__
指向的是 Object.prototype。function Student(id,name) { /** * var this = { * __proto__ : Student.prototype * } */ this.id = id; this.name = name; /** * return this; */ } var stu = Student(1,"张三"); console.log(Student.prototype.__proto__ == Object.prototype); //true
怎么理解
Student.prototype.__proto__ == Object.prototype
呢?Object的原型,即Object.prototype,可理解为是JavaScript中所有对象的起源。
通过对象字面量的形式创建对象:
var stu = {“id”:1,“name”:“张三”};
这种方式得到的stu,它的原型就直接等于Object.prototype
利用原型链来理解,相当于:stu -> Object.prototype
通过构造函数的方式创建对象:
var stu = Student(1,“张三”);
这种方式得到的stu,它的原型等于Student函数的prototype,中间夹杂了一层函数,Student函数的prototype的原型 就等于 Object.prototype了。
利用原型链来理解,相当于:stu -> Student.prototype -> Object.prototype
我们不难发现,stu对象的原型,即Student.prototype,其实就是Object对象。
如果还是不好理解,试着把原型两个字当作祖先。
即:stu对象的祖先是Student.prototype(其实就是obj对象),obj对象的祖先,就是Object的祖先。
如果按照继承的角度来理解,我们说,js中所有对象有一个共同的祖先,他就是Object.prototype。
按照对象字面量的方式创建对象,这个对象直接继承自 Object.prototype
按照定义构造函数然后new的方式创建对象,这个对象 先继承自Object,间接继承自Object.prototype
Person.prototype,即函数名.prototype,每个函数都具有prototype属性,跟他是不是构造函数没关系。js中没有定义哪个函数是不是构造函数,只有在使用new关键字调用函数的时候,才把它当作一个构造函数进行处理。
Object.prototype是所有对象原型链最底端的原型。(有例外)
Object.prototype.__proto__ == null
Object.prototype 中的方法和属性包括:
另外,对象和它原型的constructor默认是同一个函数:
Person.prototype.constructor === person.constructor;
//true
来个小例题:
var obj = {name : "a"}
var obj2 = obj;
obj = {name : "b"};
//问:obj2是多少?
//其实还是{name : "a"}
Person.prototype.name = "sunny";
function Person() {
}
var person = new Person();
Person.prototype = {
name : "cherry"
}
//问:person.name = ?
//其实还是sunny
现在换个写法,把Person.prototype的修改放到上面:
Person.prototype.name = "sunny";
function Person() {
}
Person.prototype = {
name : "cherry"
}
var person = new Person();
//问:person.name = ?
//就是cherry了
其实通过这个例题再结合上面的思考,可以得出一个答案,一个对象的原型,其实本质是
__proto__
属性指向了 函数名.prototype ,直接修改对象的__proto__
属性即可直接修改原型,而要想通过给 函数名.prototype 对象重新赋值的方式修改原型,必须在对象new之前才可以,本质原因其实还是引用传递和值传递问题。
一般不建议直接修改对象的
__proto__
属性。在不修改原型的情况下,以下等式是始终成立的:
构造函数名.prototype === 对象.__proto__
不建议直接修改对象的
__proto__
属性,原因在于:没有太大意义。function Person(name,age) { this.name = name; this.age = age; } var p = new Person("zs",18); console.log(p.__proto__ === Person.prototype); //true p.__proto__ = {"hehe":123}; console.log(p.__proto__ === Person.prototype); //false var p2 = new Person("xh",19); console.log(p2.__proto__ === Person.prototype); //true
通过
__proto__
属性直接修改对象的原型,确实很方便快捷,但是这个这个对象的原型是修改了,但其他new出来的对象的原型可没有修改,所以这种方式只适用于修改单个对象的原型。批量修改对象原型:必须在对象new之前,修改构造函数.prototype的指向。
不修改对象原有的原型的指向,如何修改原型?必须通过构造函数名.prototype.xxx的方式修改原型。
总结,修改原型的三种方式:
- 对象.
__proto__
= xxx ,这种方式只适用于修改单个对象的原型 - 构造函数.prototype = xxx ,这种方式可以修改该构造函数创建出的所有对象的原型,但必须在第一个对象new之前修改。
- 构造函数.prototype.xxx = yyy ,这种方式不但能修改该构造函数创建出的所有对象的原型,而且没必要在对象new之前修改。因为我们拿到了引用值,修改了其指向的对象的内容,并非给对象直接赋值。缺点在于如果要给对象的原型添加或修改很多的属性或方法,写起来不太方便,不太优雅,比较占用代码行数。
一个对象的原型,是一个对象是吧,这个原型对象,它上面也有原型。
Grand.prototype.lastName = "Deng";
function Grand() {
}
var grand = new Grand();
Father.prototype = grand;
function Father() {
}
var father = new Father();
Son.prototype = father;
function Son() {
}
var son = new Son();
以上代码是把多个对象的原型穿成链的过程,son对象如果要访问lastName属性,它自己没有,就去找它的__proto__
属性里找,而它的__proto__
属性指向了Son.prototype,Son.prototype又指向了father,从father对象里找,发现找不到,又从father对象的__proto__
属性里找……
son的原型是father,father的原型是grand,grand的原型是Object对象 + lastName属性,暂时把grand的原型命名为 gproto,而gproto的原型,就是Object对象的原型,即:
grand.__proto__.__proto__ == Object.prototype //true
再次注意:Object对象有原型,而Object的原型之上没有原型:
Object.prototype.__proto__ == null //true
这就是原型链,最终,son对象从Grand的原型里找到了lastName属性。反之,如果找不到,就从Grand的原型的原型里找,Grand的原型的原型就是Object的原型,此时已经找到了原型链的终端,如果还找不到,就返回undefined了。
这条原型链相当于:son -> father -> grand -> Grand.prototype -> Object.prototype
我们不难发现,grand对象的原型,就是object对象,即一个对象,它构造函数的protype属性没有被覆盖,它的原型一定就是Object对象,至于这个Object对象里面有没有属性和方法,要看我们自己有没有给这个对象的原型添加属性或方法。
例题:
Person.prototype = {
height : 100
}
function Person() {
this.eat = function () {
this.height ++;
}
}
var person = new Person();
person.eat();
console.log(person.height) // 101
console.log(person.__proto__.height) //100
是不是所有的对象,最终都会继承自Object.prototype呢?
这句话看似是对的,不管是通过对象字面量还是通过构造函数创建出来的对象,似乎都符合这个特性,但唯独有一种特殊情况:
var obj = Object.create(null);
所以我们得出一个结论:绝大多数对象最终都会继承Object.prototype。
拓展:toString()方法
javascript中有三个东西没有toString()方法,分别是:
- undefined
- null
- var obj = Object.create(null)
原始值数字、布尔、字符串没有toString(),但是能调用toString(),因为它们都会自动包装成对象。对象的原型链最底端是Object.prototype,所以一定有toString()方法。
字符串、布尔、浮点类型的数字,它们都可以直接调用toString();
例如:
123.4567.toString();
但是整型的数字不行,没办法,只能用括号包装,或者用变量接收:
(123).toString();
var num = 123; num.toString();
还可以:
2..toString() 2 .toString() (2).toString()
为什么整型的数字直接调用不行呢?
123.toString(); //报错Uncaught SyntaxError: Invalid or unexpected token
因为JavaScript解释器会优先把123后面的那个点当作浮点数的小数点处理,它认为小数点后面不能再有字符了。当然,这涉及到解释器层面的具体实现,有点太细枝末节了,总归多了解一点点有益无害。
另外,我们应该注意,不论是通过对象字面量的形式还是通过我们自定义的构造函数的方式创建的对象,它的toString()方法返回的值都是:
"[object Object]"
:function Person(name,age) { this.name = name; this.age = age; } var p = new Person("zs",18); console.log(p.toString()); //[object Object] var obj = {name : "test", age : 18}; console.log(obj.toString()); //[object Object]
而包装类的toString()返回的是字面值:
new Number(789).toString(); //789
因为Number.prototype里面定义了toString()方法,相当于重写了Object.prototype的toString()方法。这就是JavaScript中函数的重写了。
重写Person的toString() 方法:
function Person(name,age) { this.name = name; this.age = age; this.toString = function () { return `[${this.name},${this.age}]`; } } var p = new Person("张三",18); console.log(p.toString()); //[张三,18]
另外,使用document.write(obj)时,会隐式地调用obj的toString()函数。
拓展,JavaScript可正常计算的数字范围:
小数点 前16位,后16位
call/apply
call和apply都可以用来调用函数,在 js 中,函数调用不是可以直接使用()调用嘛,为什么还需要它们两个?
首先,call/apply可以起到修改this指向的作用。
函数中的 this 只有在执行期间才能确定其身份,谁调用了该函数,this就代表谁。
“谁调用了该函数,this就代表谁“。而call和apply就是专门用来打破这一规则的。
当一个函数调用call或apply的时候,这个函数执行期间,this指向的就不是自己,而是call/apply方法传入的第一个参数 obj。
function Animal(age) {
this.age = age;
}
function Person(name,age) {
Animal.call(this,age)
this.name = name;
}
var p = new Person("zs",18);
console.log(p)
//如上,Animal方法调用call,第一个参数传递的是person里的this,则Animal函数执行中,给new出来的person对象加上了age属性。
其关键点在于,call和apply用来调用别的函数,实现自己的功能,类似于模块组装,使得各个功能解耦。这是普通的函数调用方式 做不到的。
这里说下两者的区别:
传参的方式不同:
- call(obj,arg1,arg2…) 从第二个参数开始,往后的所有参数都是被调用函数的参数
- apply(obj,args) 它的第二个参数是一个参数列表,按照数组的形式传递参数。
一个生动的案例:
function Person(name) {
this.name = name;
this.eat = function (food) {
if (food) {
if (this.height){
this.height += food.length;
} else {
this.height = food.length;
}
}
}
}
var p = new Person("zs");
console.log(p); //Person {name: "zs", eat: ƒ}
p.eat("apple");
console.log(p); //Person {name: "zs", height: 5, eat: ƒ}
p.eat("banana");
console.log(p); //Person {name: "zs", height: 11, eat: ƒ}
var obj = {};
p.eat.call(obj,"苹果香蕉大鸭梨");
console.log(obj); //{height: 7}
p.eat.apply(obj,["apple"]);
console.log(obj); //{height: 12}
继承模式
继承发展史
-
传统形式 —> 原型链
- 过多地继承了没用的属性
-
自身的构造函数借用别的构造函数(call/apply)
- 不能继承借用的构造函数的原型
- 每次构造函数都要多执行一遍别的构造函数
这种方式勉强也算继承,它不是继承的形式,但是勉强可以达到继承的效果,开发中也是常用的。
-
公有原型(共享原型) —> 标准继承模式
- 具有共有原型的多个构造方法 互相影响,不能在继承的基础上,定制自己的属性
什么是共有原型呢?
以前,我们想要让Son继承Father,是这么干的:
Father.prototype.lastName = "Deng"; function Father() { } var father = new Father(); Son.prototype = father; function Son() { } var son = new Son();
这是传统的原型链的继承方式,缺点是过多地继承了一些没用的属性。
现在我们不这么玩了,我们可以这样:
Son.prototype = Father.prototype;
简单粗暴,直接让Son的原型指向Father的原型,这就是所谓的公有原型。
现在Son其实继承不到Father里面的属性和方法,只可以继承Father的原型里面的属性和方法。
这种方式的缺点在于:如果修改Son.prototype,会直接影响到Father。两个构造函数的原型绑定到一起了,改了谁另一个都会跟着改,很不合理。
-
圣杯模式
所谓的圣杯模式,其实就是在公有原型的基础上,加一个中间层:
Father.prototype.lastName = "Deng"; function Father() { } function Son() { } function Temp() { } Temp.prototype = Father.prototype; Son.prototype = new Temp(); var son = new Son();
慢慢地,这种写法被用得越来越多,所以被封装成一个函数:
function inherit(Target,Origin){ function Temp(){} Temp.prototype = Origin.prototype; Target.prototype = new Temp(); }
这样的写法看似好用,但实际上是有潜在的问题的。
比如:子构造函数的 constructor 指向紊乱。
现在,new出来的son对象,其constructor 指向的是Father,这是不合理的,因为一个对象的constructor 应该要指向它本身的构造函数。
所以方法里应该加一句,让son的constructor归位:
Target.prototype.constructor = Target;
最后一步,我们最好是让Target构造函数知道它继承自谁(这一步可有可无)
Target.prototype.uber = Origin.prototype;
好了,一个完整的圣杯模式新鲜出炉:
function inherit(Target,Origin){ function Temp(){} Temp.prototype = Origin.prototype; Target.prototype = new Temp(); Target.prototype.constructor = Target; Target.prototype.uber = Origin.prototype; }
以上就是圣杯模式了,但是写法通俗易懂,不够高大上。
雅虎公司YUI3库里圣杯模式的写法:
var inherit = ( function() { var Temp = function(){} return function(Target,Origin) { Temp.prototype = Origin.prototype; Target.prototype = new Temp(); Target.prototype.constructor = Target; Target.prototype.uber = Origin.prototype; } }() )
命名空间
实际开发中一个html页面需要引入多个js脚本,虽然这些js脚本来自五湖四海,团队开发中这些js文件都不是一个人写的,但是在浏览器把它们加载到本地以后,所有js脚本都会执行在同一个全局作用域里面。这个时候很让人头疼的问题之一,就是命名冲突问题。
变量,函数,都有可能出现命名冲突。
有哪些解决办法:
最原始的办法是变量名前 + 作者昵称,但是变量名本身的语义化会降低,并且不好维护。
然后就出现了命名空间的概念。Java中属性和方法都封装到不同的类中,甚至 for 循环和 if 语句都有自己的变量作用域,就算类重名,每个类都有属于自己的包。这样就从很大程度上杜绝了命名冲突。
很显然,Javascript的首要作者Brendan Eich并没有借鉴Java中命名空间的解决方案,也没有给出解决方案。
然后,js的使用者们,无奈之下,利用对象来模拟命名空间,类似于这样:
//org可以代表公司名称或其它
var org = {
dept1: {
zs : {
name: null,
age : 0
},
ls: {
name: null,
age : 0
}
},
dept2: {
lucy: {
flag: true,
func1: null
},
tom: {
flag: false,
obj: {}
}
}
}
很显然,这样不单单用起来不方便,开发过程中还要不断维护这些变量,很是麻烦。
有没有再好一点的解决方案呢?
真正的解决办法:webpack,这里不作讨论。
实际上呢,闭包也不失为一种解决方案。
闭包的作用之一就有:模块化开发,防止污染全局变量。
使用闭包,把一整个模块封装起来,这个模块负责一个独立的功能(这里指的是封装一套完整的,独立模块里的,所有js代码,这个js代码可能就是一个作者写的),然后返回一个或多个函数,在闭包内部的变量名,不会污染到闭包外部,在全局上可以大大减少变量的使用,自然有利于解决命名冲突。
小拓展(链式调用函数):
模拟方法连续调用,即函数的链式调用。
jQuery里就采用了这种形式:
$("div").css('background-color','red').width(100).height(100).html('hello wolrd').css('color','red');
使用这样的链式调用很大程度上方便了我们的开发,那么它是怎么做到的呢?
很简单,方法里面返回 this 即可。
小拓展(第二种访问属性的方法)
例如现在要实现一个功能:
function Person() { this.hobby1 = "足球"; this.hobby2 = "篮球"; this.hobby3 = "乒乓球"; this.hobby4 = "吉他"; this.hobby5 = "架子鼓"; this.playing = function (n) { //n == 1 打印正在玩:hobby1对应的值 } }
可以写if-else,或者switch-case,都可以实现,但是那样代码是写死的,不好。
这就要用到对象的第二种访问属性的方法:
对象['属性名']
好处:属性名就是一个字符串,相当灵活:
this.playing = function (n) { console.log("我正在玩:"+this['hobby'+n]) }
实际上,我们平时使用的person.name 这种点的形式获取属性的方式,最终都会被系统转化成方括号+字符串的形式。
小拓展(对象枚举)
enumeration
js中,遍历数组可以直接使用for循环
但是,遍历对象呢?
var obj = { name : "zs", age : 18, sex : "mali", height : 189.3 }
我们可以使用
for...in
语句:for(var prop in obj){ console.log(prop); console.log(obj[prop]); console.log(typeof obj[prop]); }
每次遍历到的prop,是对象属性,字符串的类型。
这里要注意,获取到属性值,千万不要obj.prop,而是要用obj[prop],这是非常容易犯的错误。
注意点:
for in 循环遍历对象会延展到对象以及对象的原型链上所有属性,但不会到原型链的最顶端:object.prototype,除非自己手动给object.prototype设置属性。
遍历过程中,过滤掉对象原型链上的属性:
boolean = hasOwnProperty(string)
for (var key in son){ if (son.hasOwnProperty(key)) console.log(key) }
另外,关键字 in 可以用来判断属性是否在对象里存在。
'height' in obj //属性一定用字符串形式
同时,in 也是不光会遍历对象的属性,还会顺着对象的原型链往上查找,一直延展到原型链的最顶端。
关键字 instanceof 的用法:
a instanceof B
判断a对象是否是由B构造函数构造出来的。
实际上它是看a对象的原型链上有没有B的原型。
this
例题1:
function foo(x) {
bar.apply(null,arguments);
}
function bar() {
console.log(arguments)
}
foo(1,2,3,4,5);
结果:打印出arguments对象,里面的值是[1,2,3,4,5]
思考:把bar.apply的第一个参数null去掉,打印结果会是什么呢?
关于this:
- 函数预编译过程 this --> window
- 全局作用域里 this --> window
- call / apply 可以改变函数运行时this指向
- obj.func(); func() 里面的this指向obj
例题2:
//请写成以下代码的输出结果
var name = "222";
var a = {
name : "111",
say : function () {
console.log(this.name);
}
}
var fun = a.say;
fun();
a.say();
var b = {
name : "333",
say : function (fun) {
fun()
}
}
b.say(a.say);
b.say = a.say;
b.say();
答案:
222
111
222
333
arguments
arguments中有一个属性:callee,它返回的是函数的引用。
function test() {
console.log(arguments.callee == test);
}
test();
返回的值是true。
这个arguments.callee有什么应用呢?例如:
var result = (function factorial(n) {
if (n == 1){
return 1;
} else {
return factorial(n-1) * n;
}
}(10))
console.log(result)
如果立即执行函数 factorial 不起函数名,还能完成功能嘛,答案是可以的,利用 arguments.callee。
var result = (function(n) {
if (n == 1){
return 1;
} else {
return arguments.callee(n-1) * n;
}
}(10))
console.log(result)
跟callee经常一起考的还有caller
但是,callee是arguments的属性,而caller是函数的属性。
caller用来返回函数执行的环境(即函数被哪个函数所调用)。
function test() {
console.log(test.caller); // null
demo();
}
function demo() {
console.log(demo.caller); //function test()
}
test();
在es5标准模式下,caller和callee是禁用的:
"use strict"
例题:
var a = 3;
function test() {
a = 0;
console.log(a);
console.log(this.a);
var a;
console.log(a);
}
问:运行test() 和 new test() 的结果分别是什么?
克隆
浅拷贝:只克隆原始值,数组和对象只会拷贝其引用。
function shallowClone(origin,target) {
var type = typeof origin;
if (type === "object") {
target = target || {};
for (var key in origin) {
target[key] = origin[key];
}
} else {
target = origin;
}
return target;
}
深拷贝:不单单克隆原始值,数组和对象中的值也会被拷贝过来,新拷贝出的对象不会干扰原始对象。
function deepClone(origin,target) {
var toStr = Object.prototype.toString,
arrayStr = toStr.call([]);
if (typeof origin === "object" && origin !== null) {
target = target || (arrayStr === toStr.call(origin) ? [] : {});
for (var key in origin) {
if (typeof origin[key] === "object" && origin[key] !== null) {
target[key] = arrayStr === toStr.call(origin[key]) ? [] : {};
deepClone(origin[key],target[key]);
} else {
target[key] = origin[key];
}
}
} else {
target = origin;
}
return target;
}
数组
两种创建方式:
- var arr = [1,2,3]
- var arr = new Array(1,2,3)
区别:
new Array(10); 只传了一个参数,就会把它当作数组的长度。
但是,数组的长度必须是自然数,不能是小数或浮点数,否则会报错。
数组中所有方法均来源于:Array.prototype
如果构建一个空数组,推荐使用数组字面量 [] 的形式创建数组。
在JavaScript中,关于数组的一系列操作基本不会出现报错,不会有数组越界的错误出现。js 里的数组其实是基于对象的,索引相当于对象的属性,所以,js 中数组可以看做是特殊的对象。当索引值越界,即对象的属性不存在,即返回undefined。
数组的自动扩容:
var arr = new Array(10);
arr[100] = "a";
arr.length; //101
数组的常用方法:
大致分为,可以改变原数组的:
push、pop、shift、unshift、sort、reverse、splice
不可改变原数组的:
concat、join、toString、slice
-
push() 在数组的末尾添加数据。参数是变长的。返回值为数组的长度。
//模拟数组的push方法 Array.prototype.mypush = function(...args){ for(var i = 0; i< args.length;i++) { this[this.length+i] = args[i]; } return this.length; }
-
pop() 把数组的最后一个元素移除,并返回被移除的那个元素,传参无用。
var arr = [1,2,3]; arr.pop(); // 3 arr //[1,2]
-
shift() 与pop相反,把数组的第一个元素移除并返回。
var arr = [1,2,3]; arr.shift(); //1 arr //[2,3]
-
unshift() 与push相反,在数组的最前面添加元素,参数变长。返回值为数组的长度。
var arr = [1,2,3]; arr.unshift(4,5,6); //6 arr // [4, 5, 6, 1, 2, 3]
-
sort 排序。默认按照数组中元素的字符编码 进行排序,99%的情况下,我们需要自定义排序。参数:
function(a,b)() {}
这个函数必须写两个形参
规则:看函数的返回值。
- 若 a 小于 b,在排序后的数组中 a 应该出现在 b 之前,则返回一个小于 0 的值。
- 若 a 等于 b,则返回 0。
- 若 a 大于 b,则返回一个大于 0 的值。
V8 引擎 sort 函数只给出了两种排序 InsertionSort 和 QuickSort,数量小于10的数组使用 InsertionSort,比10大的数组则使用 QuickSort。
-
reverse() 将数组中的元素逆转顺序
-
splice() 该单词为切片的意思,包含3个参数:
- 从第几个索引位置开始截取(包含该位置在内)
- 截取多少的长度
- 在切口位置添加的新数据,变长参数(可选的)
返回值:被截取的元素(数组类型)
//把4插入到3和5中间 var arr = [1,2,3,5]; arr.splice(3,0,4); //返回[] arr // [1, 2, 3, 4, 5]
-
concat() 拼接两个数组,返回一个新数组。该单词为计算机行业创造。
-
join() 参数为字符串类型(原始值皆可),将数组中元素以一个连接符进行拼接,返回一个字符串。
多个字符串的拼接,建议放到数组中,调用 join 方法拼接,效率会得到提升。
-
toString() 返回值为 arr.join(); 不填参数,默认以逗号拼接。
-
slice() 截取一部分并返回,不会改变原数组。需要传递两个参数:
- 从哪一个索引位置开始截取(包含该位置)
- 截取到哪一个索引位置(不包含该位置)
只传递一个参数,则从那个位置开始截取到最后。
不传参数,空截取(可以把类数组转化为数组)
数组的length属性如果被赋值,并且赋的值比原来的小,那么其length-1索引后的元素相当于被删除了,即使把length恢复,也找不回原来的元素。
var arr = [1,2,3,4,5];
console.log(arr.length);
arr.length = 2;
console.log(arr);
arr.length = 3;
console.log(arr); //[1,2,empty]
类数组
- 可以利用属性名模拟数组的特性
- 可以动态增长 length 属性
- 如果强行让类数组调用 push 方法,则会根据 length 属性值的位置进行属性的扩充。
比如函数里的arguments,它看起来像数组,但是数组有的方法它都没有。
var obj = {
'0' : 'a',
'1' : 'b',
'2' : 'c',
'length' : 3,
'push' : Array.prototype.push,
'splice' : Array.prototype.splice //给一个类数组加上splice方法,它在控制台的展现形式就和普通数组一样了
}
// 这样一个类数组的基本形态就构建完成了
类数组必须要有几个组成部分:
- 属性要为索引(数字)属性
- 必须具备 length 属性(类数组的关键属性)
- 最好加上 push 方法
类数组的好处:同时具备数组和对象的特性。
题目:
var obj = {
'2' : 'a',
'3' : 'b',
'length' : 2,
'push' : Array.prototype.push
}
obj.push('c');
obj.push('d');
//问 obj 的值
答:
obj = {
'2' : 'c',
'3' : 'd',
'length' : 4,
'push' : Array.prototype.push
}
我们可以把类数组当作数组来用,或者当对象来用。它可以任意定义属性和方法,同时还可以当一个数组来存储元素,非常灵活。
那些获取多个DOM元素的方法的返回值就是类数组。
题目:
封装一个type()函数,试图达到以下效果:
type( [] ) —> array
type( {} ) —> object
type( function ) —> function
type( new Number() ) —> number object
type( 1 ) —> number
function type(obj) {
var type = typeof obj;
if (type === "object") {
var typeStr = Object.prototype.toString.call(obj)
if (obj === null) {
return "null";
} else if (typeStr === "[object Array]" && obj instanceof Array && obj.constructor === Array.prototype.constructor){
return "array";
} else if (typeStr === "[object Object]") {
return "object";
} else {
return typeStr.substring(1,typeStr.length-1).split(" ")[1].toLocaleLowerCase().concat(" object");
}
} else {
return type;
}
}
数组去重
题目:给数组添加一个去重方法:unique()
- 双重for + 绝对等于判断 + 数组覆盖
Array.prototype.unique = function () {
let arr = [];
for (var i = 0; i < this.length; i++){
for (var j = i+1; j < this.length; j++){
if (this[i] === this[j]){
break;
}
}
if (j === this.length){
arr.push(this[i]);
}
}
for (var i = 0;i < arr.length; i++) {
this[i] = arr[i];
}
this.length = arr.length;
}
- indexOf() / includes()
Array.prototype.unique = function () {
let arr = [];
for (var i = 0; i < this.length; i++) {
if (arr.indexOf(this[i]) === -1) {
arr.push(this[i]);
}
}
return arr;
}
- 双重for + splice() 一边遍历,一边删除
Array.prototype.unique = function () {
for (var i = 0; i < this.length; i++){
for (var j = i+1;j < this.length; j++){
if (this[i] === this[j]){
this.splice(j,1);
j --;
}
}
}
}
- 函数递归
Array.prototype.unique = function () {
this.sort();
let arr = this;
function removeDuplicate(index) {
if (index >= 1) {
if (arr[index] === arr[index-1]){
arr.splice(index,1);
}
removeDuplicate(index-1);
}
}
removeDuplicate(arr.length-1);
}
- filter() 过滤 + indexOf()判断
Array.prototype.unique = function () {
return this.filter((e,index) => {
return this.indexOf(e) === index;
})
}
- 利用map集合的不重复规则
Array.prototype.unique = function () {
let arr = [];
let map = new Map();
for (var i = 0; i < this.length; i++) {
if (!map.has(this[i])) {
map.set(this[i],null);
arr.push(this[i]);
}
}
return arr;
}
- 利用对象的特性:同一个属性名 不可能出现两次
Array.prototype.unique = function () {
var arr = [],
temp = {},
len = this.length;
for (var i = 0; i < len; i++) {
if (!temp[this[i]]){
temp[this[i]] = true;
arr.push(this[i]);
}
}
return arr;
}
可配置性属性
js 对象中的属性可以使用关键字delete删除,这种属性叫做可配置性属性。
但是通过var关键字声明给全局window的属性,不可删除,这是不可配置性属性。
a = 9;
delete window.a; // true
console.log(a); //Uncaught ReferenceError: a is not defined
var a = 9;
delete window.a; //false
console.log(a); //9
(function (x) {
delete x; // false
console.log(x); //1
})(1)
// 函数传参 相当于var x = 1
Object.create(proto,definedProperty); 第一个参数是创建出来的对象的原型,第二个参数是对象属性的特性。这些特性包括:可删除,可读、可写、可枚举等。
题目:
function Person(name) {
var money = 100;
this.name = name;
this.consume = function () {
money--;
}
this.showMoney = function () {
console.log(money);
}
}
var p1 = new Person("zs");
var p2 = new Person("xh");
p1.consume();
p2.showMoney();
问:打印的结果是什么?
答:100
这是一个闭包封装私有属性的例子,函数中的变量 money 被封装在函数的执行期上下文里,通过new构造出的两个对象,构造函数执行了两次,所以p1和p2俩对象的执行期上下文不同,其中的money也不同。两个对象操作money,互不影响。
特殊的类型转换:
[] + "" // "" [] + "1" // "1" {} + "" // 0 {} + "1" // 1
一般情况下不用考虑引用值类型的类型转换。
1、一个字符串由 [a-z] 组成,请找出该字符串中第一个只出现一次的字母。
function findFirst(str) {
for (var i = 0; i < str.length; i++){
for (var j = 1; j < str.length; j++){
if (i == j) {
continue;
}
if (str[i] === str[j]){
break;
}
}
if (j === str.length){
return str[i];
}
}
return null;
}
function findFirst(str) {
var res = [];//存放满足条件的元素
var temp = [];//存放多次出现的元素
for (var i = 0; i < str.length; i++){
var index = res.indexOf(str[i]);
if (index === -1 && temp.indexOf(str[i]) === -1) {
res.push(str[i]);
} else {
if (index !== -1)
res.splice(index,1);
temp.push(str[i]);
}
}
return res.length>0 ? res[0] : null;
}
2、字符串中字符去重
function uniqueStr(str) {
var arr = str.split("");
return arr.filter((e,index) => {
return arr.indexOf(e) === index;
}).join("");
}
异常
JavaScript和Java中的异常类似,语法和规则如出一辙。
通常情况下,js 里报错的异常信息格式如下:
Uncaught ReferenceError: b is not defined at <anonymous>:1:13
单词:Uncaught 未捕获的
caught 捕捉,catch的过去分词
anonymous 匿名者
try-catch-finally的基本语法:
try {
}catch(e) {
}finally {
}
其中异常对象e,包含两个常用属性:
- message
- name
例如报错信息ReferenceError: b is not defined
中异常的name就是``ReferenceError,异常的message就是
b is not defined`.
JavaScript中6种常见的异常:
- EvalError:eval()的使用与定义不一致
- RangeError:数组越界
- ReferenceError:非法或不能识别的引用值
- SyntaxError:语法解析错误
- TypeError:操作数类型错误
- URIError:URI处理函数使用不当
几种常见的异常执行情况(与Java中完全一致)
function test1() {
try {
console.log('test_1_try');
return 'test_1_try_return';
} finally {
console.log('test_1_finally');
}
}
console.log(test1());
test_1_try
test_1_finally
test_1_try_return
function test2() {
try {
console.log('test_2_try');
return 'test_2_try_return';
} finally {
console.log('test_2_finally');
return 'test_2_finally_return';
}
}
console.log(test2());
test_2_try
test_2_finally
test_2_finally_return
function test3() {
try {
console.log('test_3_try');
throw new Error('test3_error');
} catch (error) {
console.log(error.message);
return 'test_3_catch';
} finally {
console.log('test_3_finally');
}
}
console.log(test3());
test_3_try
test3_error
test_3_finally
test_3_catch
习题:
(function () {
try {
throw new Error();
} catch (x) {
var x = 1,
y = 2;
console.log(x);
}
console.log(x);
console.log(y);
}())
答案:
1
undefined
2
es5 标准模式
在代码逻辑的首行,加上一行字符串:
"use strict"
这是唯一一种启用es5.0严格模式的方法。
这行字符串可以写在全局js文件的首行,表示全局使用es5严格模式,也可以写在某一函数体内的首行,表示局部使用es5严格模式。
- 开启es5严格模式后,不再兼容es3的一些不规则语法,使用全新的es5规范
- 两种写法:
- 全局严格模式
- 局部函数内严格模式(推荐)
- 就是一行字符串,不会对不兼容es5的浏览器产生影响。
- es5严格模式不支持的语法包括:
- with语句
- arguments.callee
- 方法名.caller
- es5严格模式规定:
- 变量赋值前必须先声明
- 局部this必须被赋值,Person.call(null/undefined)赋值什么就是什么
- 拒绝重复的方法参数和对象属性
为啥写个字符串启动es5严格模式
字符串表达式,不报错。向后兼容。
老版本不报错,新版可识别。
with(){ }语法
使用with语法,()内放置一个对象,会把对象放到 {} 中代码块的作用域链最顶端。
var org = {
dp1: {
jc : {
name : 'abc',
age : 123
},
deng : {
name : 'xiaodeng',
age: 234
}
},
dp2: {
}
}
// ...
with(org.dp1.jc) {
console.log(name);
}
// 这就是命名空间的用法,使用with可达到代码简化的目的。
with(document) {
write('hello world')
}
但是with过于强大,它改变了原有的作用域链,如果作用域链过长,系统内核会消耗大量的性能来实现这一操作,这拖慢了JavaScript代码的执行速度。在es5严格模式中,禁用了with语法。
变量赋值前必须先声明:
var a = b = 3;//不通过
a = 5;//不通过
es5严格模式降低了代码的灵活度和松散性,由此带来了代码的规范性和严格性,减少的出错的概率。
局部的this,预编译的时候,不指向window,而是undefined:
function test() {
console.log(this)
}
test(); //undefined
function Test() {
console.log(this)
}
new Test();
/*控制台打印 Test {}
控制台打印的信息:Test 代表该对象的constructor名字
*/
小区别:严格模式下函数调用call()改变this指向,call中传递原始值,方法中的this就是原始值。es3.0中同样的操作,传递原始值,方法中this会被包装成包装类对象。
"use strict"
function test() {
console.log(this) //就是原始值123
}
test.call(123)
function test() {
console.log(this) //Number (123)
}
test.call(123)
es3中重复的参数、重复的属性名不会报错,es5中会报错。但这一点还取决于各自浏览器的不同实现。
eval()函数,可以把字符串解析成表达式并执行。
es3.0中不能使用eval(); eval是魔鬼
习题:
function getAge() {
'use strict'
age = 21;
console.log(age);
}
getAge();
答案:报错 Uncaught ReferenceError: age is not defined
DOM
什么是DOM?
- DOM -> Document Object Model
- DOM定义了表示和修改文档所需的方法。DOM对象即为宿主对象,由浏览器厂商定义,用来操作HTML和XML功能的一类对象的集合。也有人称DOM是对HTML以及XML的标准编程接口。
注意:DOM用来操作的是HTML和XML,不能操作CSS,任何东西都操作不了CSS。
它只能给DOM对象添加行间样式,它操作的本质上还是DOM对象,它给DOM对象加上了一些css属性,这些属性刚好被CSS渲染器所识别。它是通过操作HTML来间接地改变CSS样式效果。
DOM节点
节点的三个属性
- nodeName 节点的名称(元素节点的标签名,以大写形式表示),只读
- nodeValue Text节点或Comment节点的文本内容,可读可写
- nodeType 标识该节点的类型,只读
节点的一个方法
- Node.hasChildNodes()
节点类型nodeType
- 元素节点 — 1
- 属性节点 — 2
- 文本节点 — 3
- 注释节点 — 8
- #document — 9
- DocumentFragment — 11
元素节点的一些属性
- innerHTML
- innerText
- textContent
- attributes
元素节点的一些方法
- setAttribute()
- getAttribute()
DOM基本操作
- 查(查看元素节点)
- document代表整个文档
- document.getElementById() 在IE8以下的浏览器,不区分id大小写,而且也能返回匹配name属性的元素
- getElementsByTagName() 支持通配符
*
选中所有元素节点 - getElementsByName() 理论上只有部分标签name可生效(表单,表单元素,img,iframe)
- getElementsByClassName() IE8和 IE8以下的版本中没有。可以多个class一起
- querySelector() IE7和 IE7以下的版本中没有。选中的元素不是实时的。
- querySelectorAll() IE7和 IE7以下的版本中没有。选中的元素不是实时的。
- 基于dom获取节点
- parentNode 父节点(最顶端的parentNode 为#document)
- childNodes 获取所有子节点
- firstChild 第一个子节点
- lastChild 最后一个子节点
- nextSibling 下一个兄弟节点
- previousSibling 上一个兄弟节点
- 基于dom获取元素节点 (相当于筛选出元素节点,可能存在IE不兼容情况)
- parentElement 父元素节点
- children 获取所有子元素节点
- children.length / childElementCount 子元素节点的个数
- firstElementChild 第一个子元素节点
- lastElementChild 最后一个子元素
- nextElementSibling 下一个兄弟元素节点
- previousElementSibling 上一个兄弟元素节点
- 增
- document.createElement();
- document.createTextNode();
- document.createComment();
- document.createDocumentFragment();
- 剪切操作
- 父节点.appendChild();
- 父节点.insertBefore(a, b);
- 删
- parent.removeChild();
- 改
- parent.replaceChild(new, origin);
另外提供了一下操作dom属性的便捷方式:
dom.className -> dom.setAttribute(“calss”,xxx)
dom.id-> dom.setAttribute(“id”,xxx)
我们说querySelector()方法不是实时的,而其他选择dom元素的方法是实时的,那么实时到什么程度呢?我举个栗子:
<div class="content"></div>
<div class="content"></div>
<div class="content"></div>
<script>
var oDivMap = document.getElementsByClassName('content');
console.log(oDivMap.length); //3
oDivMap[1].className = "demo";
console.log(oDivMap.length); //2
</script>
DOM接口
可以理解为 DOM对象原型链、继承关系
Node :
- Document(文档)
- HTMLDocument
- CharacterData
- Text
- Coment
- Element(元素)
- HTMLElement
- HTMLHeadElement
- HTMLBodyElement
- HTMLTitleElement
- HTMLParagraphElement
- HTMLInputElement
- HTMLTableElement
- …etc.
- HTMLElement
- Attr
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>
元素 -
getElementsByClassName()、querySelectorAll()、querySelector()在Document,Element类中均有定义
习题:
1、遍历元素节点树,要求不能用children属性
基本思路:使用childNodes 属性获取所有节点,利用nodeType == 1条件过滤出所有的元素节点。
2、封装函数,返回元素e的第n层祖先元素
基本思路:利用 parentElement 属性,为了浏览器不支持parentElement ,则使用parentNode + nodeType
function retAncestor(ele,n) {
while (ele && n > 0) {
if (ele.parentElement){
ele = ele.parentElement;
} else {
for (ele = ele.parentNode; ele && ele.nodeType !== 1; ele = ele.parentNode);
}
n--;
}
return ele;
}
3、封装函数,返回元素e的第n个兄弟节点,n为正,返回后面的兄弟节点,n为负,返回前面的,n为0,返回自己。
基本思路:与上一题的思路异曲同工
function retSibling(ele,n) {
while (ele && n) {
if (n > 0) {
//返回后面的兄弟节点
if (ele.nextElementSibling) {
ele = ele.nextElementSibling;
} else {
for (ele = ele.nextSibling; ele && ele.nodeType !== 1; ele = ele.nextSibling);
}
n--;
} else if (n < 0) {
//返回前面的兄弟节点
if (ele.previousElementSibling) {
ele = ele.previousElementSibling;
} else {
for (ele = ele.previousSibling; ele && ele.nodeType !== 1; ele = ele.previousSibling);
}
n++;
}
}
return ele;
}
4、封装函数insertAfter();功能类似insertBefore();
提示:可忽略老版本浏览器,直接在Element.prototype上编程
基本思路:类似于 insertBefore(ele,baseEle),接收两个参数,第一个参数是要添加的dom元素,第二个参数是在谁的前面添加。众所周知insertBefore()是剪切操作,所以我们必须遍历整个document文档,判断要添加的dom元素是否在页面中已经存在,如果存在,把它删除以后再添加,并且我们必须保证baseEle在调用该方法的dom元素中存在且必须是它的子元素。我们要实现insertAfter(),需要先找到baseEle的parentElement的子节点,往parentElement的children数组里的相应位置添加newDom。
/**
* insertAfter所需参数:
* ele 要插入的元素
* baseEle 在baseEle的后面插入
* 所需满足的条件:
* baseEle 必须是当前调用该方法的dom元素的子元素,注意是直接子元素 不可以是后代元素
* ele 可以是新创建的元素,也可以是整个document文档中存在的任意元素
*/
Document.prototype.insertAfter = Element.prototype.insertAfter = function (ele, baseEle) {
HTMLCollection.prototype.indexOf = Array.prototype.indexOf;
var parentChildren = this.children;
if (parentChildren.indexOf(baseEle) === -1) {
throw new Error("The node after which the new node is to be inserted is not a child of this node.");
}
var baseArr = baseEle.parentElement.children;
var insertIndex = baseArr.indexOf(baseEle) + 1;
/**
* 由于HTMLCollection中的值是只读的,无法通过索引来替换
* 所以这里的插入,是先remove掉要插入的元素后面的所有元素,再依次appendChild进去
*/
(function (arr,insertPosition,insertValue) {
var removed = [];
for (var i = insertPosition; i < baseArr.length; i++){
removed.push(baseArr[i]);
baseArr[i].remove();
}
// appendChild其实也是剪切操作,所以ele就算是已存在元素也不用我们手动删除
baseEle.parentElement.appendChild(insertValue);
for (var i = 0; i < removed.length; i++) {
baseEle.parentElement.appendChild(removed[i]);
}
}(baseArr,insertIndex,ele))
delete HTMLCollection.prototype.indexOf;
}
以上insertAfter()的实现没有调用api给我们提供的insertBefore()方法,如果我们利用insertBefore()来实现insertAfter(),代码行数会大大减少:
/**
* 思路:找到baseEle的下一个兄弟节点,然后在下一个兄弟节点之前插入ele
* 如果没有下一个兄弟节点,直接调用appendChild方法插入到后面
*/
Document.prototype.insertAfter = Element.prototype.insertAfter = function (ele, baseEle) {
if (baseEle.nextSibling) {
this.insertBefore(ele, baseEle.nextSibling);
} else {
this.appendChild(ele);
}
}
5、封装remove(); 使得child.remove()直接可以销毁自身
试想如果api中没有提供节点的remove()方法,如何删除该节点呢?
其实并没有什么好的办法,能提供给我们的编程接口很少,我尝试拿到该节点的父节点,然后调用children()方法获取它的所有子元素,想通过给删除该类数组(HTMLCollection)中的元素来实现,该类数组并没有提供删除元素的函数,能不能通过删除对象属性的方式来删除呢?
答案不可以的,经过我的试验,HTMLCollection的对象中属性是不可删除的,调用delete会返回false。
没办法。只能通过api给我们提供的另一个删除节点的函数:
Document.prototype.myRemove = Element.prototype.myRemove = function () {
this.parentNode.removeChild(this);
}
6、将目标节点内部的节点顺序,逆序。
eg:
<div><a></a><em></em></div> <div><em></em><a></a></div>
思路:经过上面例题的探索,我们可以明确一点,那就是HTMLCollection的类数组是只读的,妄想直接通过操作该数组来操作dom是不可能实现的。只能乖乖调用api中的函数。
想把一个节点内部所有节点逆序,可不可以这样呢?
[1,2,3,4] 把它看成一个数组
遍历该数组,从倒数第二个元素开始往前遍历,把每次遍历到的元素,剪切到数组的末尾:
[1,2,3,4] -> [1,2,4,3] -> [1,2,4,3] -> [1,4,3,2] -> [4,3,2,1]
appendChild()方法刚好满足我们的需求:
Document.prototype.reverse = Element.prototype.reverse = function () {
var childNodes = this.childNodes,
len = childNodes.length;
for (var i = len-2; i >= 0; i--) {
this.appendChild(childNodes[i]);
}
}
临时想出来的一些题目
1、判断两个字符串中的内容是否相同,判断的依据是这样的:
输入:
var str1 = "hello world"; var str2 = "world hello";
输出:
true
说明:以空格作为分隔符,看里面的单词是否都一致。
实现代码:
function strEquals(s1,s2) {
var arr1 = s1.split(" ");
var arr2 = s2.split(" ");
if (arr1.length !== arr2.length){
return false;
}
for (var i = 0; i < arr1.length; i++){
if (arr2.indexOf(arr1[i]) === -1){
return false;
}
}
return true;
}
2、做一些题目的时候,往往会大量使用到数组或字符串的 indexOf() 方法,您能不能自己实现一个呢?
function arrayIndexOf(arr,element,fromIndex) {
fromIndex = fromIndex || 0;
var start = fromIndex;
var end = arr.length - 1;
var middle = Math.floor((start+end)/2);
while (start <= end) {
if (arr[middle] === element) {
return middle;
} else if (arr[middle] > element) {
end = middle - 1;
middle = Math.floor((start+end)/2);
} else if (arr[middle] < element) {
start = middle + 1;
middle = Math.floor((start+end)/2);
}
}
return -1;
}
function stringIndexOf(str,sequence,fromIndex) {
fromIndex = fromIndex || 0;
str = str.split("");
sequence = sequence.split("");
for (var i = fromIndex; i < str.length; i++) {
if (str.length - i < sequence.length) {
break;
}
var n = i;
for (var j = 0; j <sequence.length; j++){
if (str[n] !== sequence[j]){
break;
} else {
n = i + j + 1;
}
}
if (j === sequence.length){
return i;
}
}
return -1;
}
Date
Date()函数,直接调用会返回日期字符串。
使用new Date()的方式调用,返回一个日期对象,该日期对象记录了它new出来那一刹那的时间。
JavaScript程序中关于时间的代码,几乎都和Date()构造函数相关。
日期对象的常用方法
- getFullYear() 获取年份,注意避免使用getYear()
- getMonth() 获取月份(0~11),实际使用通常要加1
- getDate() 返回一个月中的某一天(1~31)
- getDay() 返回一周中的某一天(0~6),0代表周日
- getHours() 返回小时(0~23)
- getMinutes() 返回分钟(0~59)
- getSeconds() 返回秒数 (0~59)
- getMilliseconds() 返回毫秒数 (0~999)
- getTime() 返回1970年1月1日0时0分0秒0毫秒至今的毫秒数
- setFullYear()
- setMonth()
- setDate()
- setHours()
- setMinutes()
- setSeconds()
- setMilliseconds()
- setTime() 以毫秒来设置Date对象的时刻
- toLocaleString() 形如:“2021/9/5 下午10:38:21”
- toString() 形如:“Sun Sep 05 2021 22:38:21 GMT+0800 (中国标准时间)”
- toUTCString() 形如:“Sun, 05 Sep 2021 14:38:21 GMT”,该时间格式为服务器返回的响应中响应头字段date的标准时间格式
关于千年虫
早期计算机时间是以6位10进制数字来表示时间,前两位用来代表年份。
据说这样设计的目的是为了节省内存空间。的确早期计算机存储设备价格非常昂贵,为了计算机的普及,早期计算机程序设计者采取了6位数代表时间的方案,这一选择在当时看来甚至是明智之举。
而在时间逼近2000的时间,人们意识到了这个问题,如果该问题不解决,会使得所有计算机中关于依赖时间的程序彻底紊乱甚至瘫痪。到那时,人类会面临前所未有的空前灾难。如金融危机,核弹误发射等等一系列问题。
大量的程序员投入到这场保卫战中,重新改写已有程序中关于时间的代码。
到2000年1月1日0时的到来,虽然大部分计算机已经暂时解决了千年虫的问题,然而不可避免地千年虫仍然给全世界带来了数百亿美元的损失。
另外,到2038年1月19日03:14:07,32位操作系统的计算机会面临时间紊乱的问题。
练习题:
封装函数,返回当前时何年何月何日何时,星期几,几分几秒。
function logTime() {
var now = new Date();
let year = now.getFullYear();
let mouth = now.getMonth()+1;
let day = now.getDate();
let week = (function (day) {
if (day === 0) {
return "星期日";
} else {
return "星期"+(function (day) {
switch (day) {
case 1 : return "一";
case 2 : return "二";
case 3 : return "三";
case 4 : return "四";
case 5 : return "五";
case 6 : return "六";
}
}(day))
}
}(now.getDay()));
let hour = now.getHours();
let minute = now.getMinutes();
let second = now.getSeconds();
return `${year}年${mouth}月${day}日-${week}-${hour}时${minute}分${second}秒`;
}
定时器
- setInterval()
- setTimeout()
- clearInterval()
- clearTimeout()
定时器中可以封装定时执行或间歇性执行的代码,这些代码会放到执行队列中交给 js引擎 去执行,它的执行机制是基于一种特殊的数据结构:红黑树。
需要注意以下2点:
- 给定时器设置的时间是不准确的
- 给定时器设置了时间后,该时间只会被读取一次,无法更改
此外,有个冷门知识是:定时器中封装的代码可以是字符串的形式,但不是把原来的匿名函数直接用引号括起来,而是直接写代码块:
var timer = setInterval(
`var i = 0;i++;console.log(i);
if (i > 10){
window.clearInterval(timer);
}`,1000)
这样代码可以运行么?可以,但不会符合我们的预期,这个变量i将会永远是1
经过我的试验发现,字符串代码里不可以声明变量,以上的功能只能这样写:
var i = 0;
var timer = setInterval(`
console.log(i++);
if (i > 10) {
window.clearInterval(timer);
}
`,1000)
然后就可以正常执行了。
看到了吧,不是多年的脑震荡也写不出这种代码。
DOM/BOM基本操作
<!DOCTYPE html>
是HTML5形式的DTD约束方式。
如果把html文档最顶部一行的
<!DOCTYPE html>
删除,将导致浏览器进入怪异渲染模式(也称混杂模式)。正常情况下是标准模式。
浏览器存在怪异渲染模式的目的,是为了版本的向后兼容。怪异渲染模式能够识别并渲染基于浏览器老版本编写的页面。一般情况下,仅仅是IE遗留的几个问题需要启动怪异模式来解决,一般情况下不需要去掉顶部的HTML5声明约束。
一个特别细的小细节:
//正常情况下,我们在函数中return一个对象,是这样写的:
return {
}
// 但是,return 一个对象,一定不要这样写:
return
{
}
// 这样的话解释器会识别为return;{}
// 相当于直接return一个undefined
现在已经不用担心会写错了,如今的集成开发环境环境会识别到这类语法错误并且在代码执行之前提前告知我们。
窗口属性
查看整个页面的滚动条 滚动距离:
- window.pageXOffset / pageYOffset 注意IE9以下不兼容
- document.body / documentElement.scrollLeft / scrollTop 兼容性比较混乱,但好在两种情况下的值不会同时存在,我们可以把两个值相加
封装兼容性方法,返回页面滚动轮滚动距离getScrollOffset():
function getScrollOffset() {
if (window.pageXOffset) {
return {
x : window.pageXOffset,
y : window.pageYOffset
}
} else {
return {
x : document.documentElement.scrollLeft + document.body.scrollLeft,
y : document.documentElement.scrollTop + document.body.scrollTop
}
}
}
查看可视区域窗口的尺寸:
- window.innerWidth / innerHeight (加上滚动条的宽度/高度) 注意IE9以下不兼容
- document.documentElement.clientWidth / clientHeight 标准模式下,任意浏览器都兼容
- document.body.clientWidth / clientHeight 适用于怪异模式下的浏览器
封装兼容性方法,返回浏览器可视窗口的尺寸getViewportOffset():
function getViewportOffset() {
if (window.innerWidth) {
return {
width: window.innerWidth,
height: window.innerHeight
}
} else {
if (document.compatMode === "BackCompat") {
return {
width: document.body.clientWidth,
height: document.body.clientHeight
}
} else {
return {
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight
}
}
}
}
这里例举一个我在实际开发中,遇到的问题,获取当前浏览器实际宽度,且不受页面缩放和电脑屏幕尺寸本身缩放所影响:
function getBrowserWidth() {
let userAgent = navigator.userAgent;
let isChrome = userAgent.indexOf("Chrome") > -1;
let isFireFox = userAgent.indexOf("Firefox") > -1;
let isIE = !!window.ActiveXObject || "ActiveXObject" in window;
let isSafari = userAgent.indexOf("Safari") > -1;
let fireFoxOriginalDevicePixelRatio = 1;
if (isFireFox) {
fireFoxOriginalDevicePixelRatio = window.devicePixelRatio;
}
let html = document.documentElement;
let width = html.getBoundingClientRect().width;
let ratio = 1;
if (isChrome || isSafari) {
ratio = window.outerWidth / window.innerWidth;
} else if (isFireFox) {
ratio = window.devicePixelRatio/fireFoxOriginalDevicePixelRatio;
} else if (isIE) {
if (screen.deviceXDPI && screen.logicalXDPI) {
ratio = screen.deviceXDPI / screen.logicalXDPI;
}
}
width *= ratio;
return width;
}
dom尺寸
查看dom元素的几何尺寸:
- domEle.getBoundingClientRect(); 兼容性很好
该方法返回一个DOMRect 对象,对象里面的属性包括:left、top、right、bottom、width、height、x、y等。其中,left和top代表该元素左上角的x和y坐标,right和bottom代表元素右下角的x和y坐标。x的值相当于left,y的值相当于top。
width和height表示该dom元素的宽度和高度。在老版本的IE浏览器中这两个值并不存在,但好在可以通过其他属性计算出来。width = right - left,height = bottom - top
需要注意:getBoundingClientRect()中返回的结果不是"实时的"。
另一种查看dom元素尺寸的方式:(返回的结果总是整数类型的)
- domEle.offsetWidth / domEle.offsetHeight
查看元素的位置:
- domEle.offsetLeft / domEle.offsetTop
对于无定位父级的元素,返回相对文档的坐标,对于有定位父级的元素,返回相对于最近的有定位的父级的坐标。(无论是left还是margin-left都算作x坐标偏移量)
返回最近的有定位的父级元素:
- domEle.offsetParent
若无最近的有定位的父级元素,则返回body,document.body.offsetParent返回null
封装函数,返回dom元素相对于文档的坐标 getElementPosition():
function getElementPosition(ele) {
var left = ele.offsetLeft;
var top = ele.offsetTop;
while (ele.offsetParent) {
ele = ele.offsetParent;
left += ele.offsetLeft;
top += ele.offsetTop;
}
return {
x: left,
y: top
}
}
让滚动条滚动,window上有三个方法:
- scroll()
- scrollTo()
- scrollBy()
这三个方法使用起来很类似,都是接收两个参数,一个x,一个y。
其中scroll()和scrollTo()方法功能类似,都是将x,y坐标传入,即可将滚动条滚动到指定位置。
区别:scrollBy()方法会再当前滚动的坐标基础上进行累加。
eg:利用scrollBy()函数制作快速阅读的功能。
脚本化css
读写元素css属性:
- domEle.style.prop
这是JavaScript中唯一可以设置dom元素css属性的方式。
返回一个 CSSStyleDeclaration 对象(仅仅是行间样式里的)
它可以读写行间样式,没有兼容性问题。
w3c建议我们,碰到类似 float这样的关键字属性,前面应加css前缀
eg: float -> cssFloat
另外建议拆解复合属性
css属性名为组合单词是例如 font-size,js 中应使用小驼峰写法 fontSize
利用该方式给dom元素的css属性赋值,写入的值必须是字符串格式。
eg: domEle.style.borderRadius = “50%”;
有单位的话必须携带单位
查询计算样式:
- window.getComputedStyle(ele,null)
计算样式只读
返回一个 CSSStyleDeclaration 对象(计算之后的,含默认值的)
返回的计算样式的值单位是绝对单位px,没有相对单位。
IE9以下不兼容
window.getComputedStyle(ele,null)
第一个参数是domEle元素,第二个参数通常情况下都为 null,除非你想要获取伪元素的样式。
// 例如,获取其after伪元素的样式表 window.getComputedStyle(domEle,"after");
可以说,第二个参数专门就是为了 JavaScript 能够获取伪元素的样式而精心设计的,但仅仅用于获取样式属性值,不可修改。
查询样式:
- domEle.currentStyle
计算样式只读
返回的计算样式的值单位会与设置的值保持一致,不会计算成绝对单位的值
但是,该属性是 IE 浏览器独有的属性
封装兼容性方法 getStyle(obj, prop):
function getStyle(elem, prop) {
if (window.getComputedStyle) {
return window.getComputedStyle(elem, null)[prop];
} else {
return elem.currentStyle[prop];
}
}
练习题
尝试手动封装一个动画函数。
这里简单谈谈我的思路。
原生 js 实现动画效果,其主要还是通过 domEle.style[prop]来实现,本质就是给dom元素设置行间样式。首先,这个函数必须接收的参数有 elem(dom元素)、prop(要设置的css属性)。其他的,譬如,startValue、endValue。
startValue要不要给定?这是颇具争议性的问题,我个人认为,不应指定startValue,应该自己算出来那个值,通过window.getComputedStyle()来计算当前的dom元素指定的css属性值为多少。
这样引发了另外一个问题,要不要设置单位?如果要,单位是当作参数传递进来好呢,还是在字符串值中写好?这个问题就更难以回答了。首先,通过window.getComputedStyle()所计算出来的值,数值类属性值默认都是px单位的。px在计算机的世界中,它是一个绝对单位。然而实际开发中,我们可能用到的单位不止px,还可能有em,rem等等。尤其是在大型项目中,如果所有代码都通过pxToRem类的插件把css中的单位动态转换,而我们写的动画函数里单位是写死的,必然会引发一系列问题。我个人的建议是,单位不要写死,也不要当作参数传递进来(为了减少参数的个数),我建议把属性值和单位拼接成字符串,再当作参数传递进来。
然后我们还应该意识到,动画的两种截然不同的实现。
第一种实现,固定时间的动画。举个例子,一个dom元素,当前宽度值为100px,想让它在动画执行完毕后,宽度值为500px,固定时间5s内完成。这个时候,需要自己手动计算速度值,显然逻辑稍微复杂一些。
第二种实现,固定速度的动画。即一个dom元素,从宽度100px到宽度500px,其以恒定的速度来变化(这里假设定时器设置的间隔时间是恒定且合适的)。
关于这两种实现,其实都是殊途同归的。
但是,对于这个函数的使用者而言,如果一个函数能够兼顾这两种实现,岂不是皆大欢喜。
能不能实现呢?答案肯定是可以的。只不过稍稍复杂一下。
首先我们考虑一下传参方式的问题,参数有点多。而且我们想要实现的效果是:如果传递了duration值,则采取固定时间动画策略,如果传递了speed值,则采取固定速度动画策略,这样一来,我个人认为,参数传递一个对象较为合理。
定义函数名:animate
形参列表:
- elem : 执行动画的dom元素
- options : 动画配置参数(一个对象)
- attribute :css属性名
- value:css属性值
- duration :动画持续时间
- speed :动画执行速度
- intervalTime :定时器执行间隔
- callback : 动画执行完毕的回调函数
其中duration 和 speed 如果同时存在,按理说应该抛出异常,但是为了遵循尽量少报错的编程原则,我们只取duration 的值而忽略speed的值,采用固定时间动画策略。
如果duration 和 speed都不存在,会给duration 赋一个默认值,同样采取固定时间动画策略。
具体实现:
function animate(elem, options, callback) {
if (elem == null){
console.log("animate: elem is null")
return;
}
let attribute = options.attribute;
let notStyle = options.notStyle;
let startValue = notStyle ? elem[attribute] : parseFloat(window.getComputedStyle(elem)[attribute]);
let endValue = parseFloat(options.value);
let unit;
if (typeof options.value == 'string') {
unit = options.value.substr(endValue.toString().length);
switch (unit) {
case "px":break;
case "rem":
startValue /= parseFloat(window.getComputedStyle(document.documentElement)["font-size"]);
break;
case "em" :
startValue /= parseFloat(window.getComputedStyle(elem)["font-size"]);
break;
default :
if (notStyle) {
break;
}
if (!unit){
throw new Error("options.value must be a string with units, support px,rem,em");
}
throw new Error("animate can not support this unit:"+unit);
}
}
if (!options.intervalTime) {
options.intervalTime = 5;
}
if (startValue == endValue) {
callback && callback();
return;
}
let timer = null;
let nowValue;
let startTime = new Date().getTime();
function moveTo(value) {
if (notStyle) {
elem[attribute] = value;
} else {
elem.style[attribute] = value + unit;
}
}
function fixedTime() {
let remaining = Math.max(0, startTime + options.duration - new Date().getTime());
let percent = 1 - (remaining / options.duration);
nowValue = (endValue - startValue) * percent + startValue;
moveTo(nowValue);
if (percent === 1) {
moveTo(endValue);
clearInterval(timer);
callback && callback();
return;
}
}
function fixedSpeed() {
if ((options.speed > 0 && nowValue >= endValue) || (options.speed < 0 && nowValue <= endValue)) {
moveTo(endValue);
clearInterval(timer);
callback && callback();
return;
}
nowValue += options.speed;
moveTo(nowValue);
}
if (!options.duration && !options.speed) {
options.duration = 1000;
}
if (options.duration) {
if (typeof options.duration !== 'number')
throw new Error("options.duration must be a number");
if (!options.duration)
throw new Error("options.duration must not be zero");
timer = setInterval(fixedTime, options.intervalTime);
} else if (options.speed) {
if (typeof options.speed !== 'number')
throw new Error("options.speed must be a number");
if (!options.speed)
throw new Error("options.speed must not be zero");
options.speed = startValue < endValue ? Math.abs(options.speed) : -Math.abs(options.speed);
nowValue = startValue;
timer = setInterval(fixedSpeed, options.intervalTime)
}
}
事件
首先先清楚一个事儿,一个dom元素的事件,是本身就存在的,而不是绑定了以后才有。
所谓的绑定事件,就是给这个事件添加一个或多个事件处理函数,当事件触发时,执行这些程序。
如果没有给dom元素绑定事件,事件依然存在,只不过没有处理函数而已,该事件仍然会一层一层的冒泡,传递给父级元素。
如何绑定事件
- elem.onXxx = funtion (event) {}
此方式兼容性很好,但是一个元素只能绑定一个处理程序
这种绑定事件的方式基本等同于写在HTML行间上(区别是写在行间的事件执行代码不需要写function)
- elem.addEventListener(type, func, false)
IE9以下不兼容,这种方式可以给一个事件绑定多个处理程序
- ele.attachEvent(‘on’+type, func)
IE独有的,一个事件也可以绑定多个处理程序
addEventListener和attachEvent的细微区别:
除了适用的浏览器、参数外,两个事件绑定方式基本类似,都可以给一个事件绑定多个处理程序
但是,addEventListener给一个事件绑定的多个处理函数的必须是不相同的。
而attachEvent给一个事件绑定的多个处理函数可以相同。同一个引用的函数可以被绑定多次。(在我看来这是bug)
事件处理程序的运行环境
- elem.onXxx = funtion (event) {}
事件处理函数的内部this指向的是dom元素本身
- elem.addEventListener(type, func, false)
事件处理函数的内部this指向的是dom元素本身
- ele.attachEvent(‘on’+type, func)
事件处理函数的内部this指向的是window (在我看来这是bug)
有必要封装一个给dom元素添加事件的兼容性方法 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;
}
}
解除事件处理程序
- elem.onXxx = “” / false / null
- elem.removeEventListener(type, func, false)
- elem.detachEvent(‘on’+type, func)
注意:给事件绑定了匿名函数,是无法解除的
封装兼容性方法,解除绑定的事件 removeEvent(elem, type, func):
function removeEvent(elem, type, func) {
if (elem['on'+type]) {
elem['on'+type] = '';
}
if (elem.removeEventListener) {
elem.removeEventListener(type,func,false);
} else if (elem.detachEvent) {
elem.detachEvent('on'+type,func);
}
}
要注意用哪种方式绑定的事件,就要用哪种方式来移除事件。
事件处理模型–事件冒泡/事件捕获
- 事件冒泡
- 结构上(非视觉上)具有嵌套关系的dom元素,会存在事件冒泡的功能,即同一个事件,自事件源元素冒泡向父元素。(自底向上)
- 事件捕获
- 结构上(非视觉上)具有嵌套关系的dom元素,会存在事件捕获的功能,即同一个事件,自父元素捕获至事件源元素。(自底向上)
- 开启事件捕获:domEle.addEventListener(type, func, true)
- IE没有事件捕获
- 触发顺序:先捕获、后冒泡、中间是事件源触发函数执行
例如有三个dom元素:wrapper>content>box
给这三个dom元素都绑定事件,并且同时绑定事件冒泡和事件捕获模型,我们观察它的执行顺序:
var wrapper = document.getElementsByClassName('wrapper')[0];
var content = document.getElementsByClassName('content')[0];
var box = document.getElementsByClassName('box')[0];
wrapper.addEventListener('click',function () {
console.log('wrapper bubble');
},false)
content.addEventListener('click',function () {
console.log('content bubble');
},false)
box.addEventListener('click',function () {
console.log('box2');
},false)
wrapper.addEventListener('click',function () {
console.log('wrapper catch');
},true)
content.addEventListener('click',function () {
console.log('content catch');
},true)
box.addEventListener('click',function () {
console.log('box1');
},true)
点击,最内层的元素box,控制台打印的顺序如下:
wrapper catch
content catch
box1
box2
content bubble
wrapper bubble
可以看出,捕获事件的执行要早于冒泡。
而关于事件源对象box上的事件是否为冒泡还是捕获,严格来讲是没有意义的。
关于本人测试代码和老师代码执行结果的不一致
捕获事件的执行要早于冒泡,这一点是肯定的。
而事件源对象上如果绑定了同一个事件的不同事件模型,是事件捕获先执行呢还是事件冒泡先执行呢?
老师代码里是: 谁先绑定谁先执行
我的测试代码里是: 总是事件捕获的先执行
考虑到教程视频是4年之前的,这期间谷歌浏览器多次更新换代,出现些许差异也不足为奇。
-
特殊的事件:focus、blur、change、submit、reset、select等事件不冒泡
-
取消事件冒泡
- event.stopPropagation(); W3C标准,不支持IE9以下版本
- event.cancelBubble = true; 兼容IE9以下,此方式chrome也支持
封装取消冒泡的函数 stopBobble(event):
function stopBobble(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
什么时候需要取消事件冒泡?
我个人认为判断依据如下:当一个dom元素可能触发某一个事件,而该事件恰巧绑定在了其父元素上,同时我们不希望在触发子dom元素的那个事件的同时触发绑定在父元素上的事件处理函数。这个时候应该取消冒泡。
- 阻止默认事件
- return false; 兼容性好,但是只有以对象属性的方式注册的事件才生效
- event.preventDefault(); W3C标准,IE9以下不兼容
- event.returnValue = false; 兼容IE,chrome也支持
封装一个阻止默认事件的函数 cancelDefaultHandler(event):
function cancelDefaultHandler(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
举个栗子,取消网页右键菜单:
document.oncontextmenu = function (event) {
console.log("嘿嘿,不让你点击右键!!");
event.preventDefault()
// event.returnValue = false;
// return false;
}
事件对象event
关于事件对象event:每一个事件处理函数中第一个参数可以接收一个事件对象(IE浏览器除外),该对象记录了事件发生时的一系列关键性数据,以供我们去使用。
在IE浏览器中事件处理函数执行后,事件对象会被挂载到全局对象window上。
所以才有了兼容性写法:var event = e || window.event
事件对象上有个属性用来记录事件源对象:
- event.target 火狐只有这个
- event.srcElement IE只有这个
这俩chrome都有
兼容性写法:var target = event.target || event.srcElement;
能够获取到事件的源对象,由此产生了事件委托
关于事件委托我的理解是这样的:
子元素想绑定事件,但可能子元素数量太多或者是动态生成的,不太好绑定事件或者很麻烦。这时候我们给这些子元素的父级绑定事件,通过事件冒泡或事件捕获机制,把原来应该绑定到子元素的事件处理函数,委托给父级元素的事件处理函数去执行,在执行过程中可通过事件对象获取到事件源对象,即获取到我们真正触发事件的那个子元素,然后做一系列业务操作。
事件委托的优点:
- 性能好,不需要循环所有子元素一个一个地绑定事件
- 灵活,当有新的子元素时不需要重新绑定事件
事件分类
-
鼠标事件
- click 鼠标左键单击
- mousedown 鼠标按下
- mousemove 鼠标移动
- mouseup 鼠标抬起
- mouseover / mouseenter 鼠标移入
- mouseout / mouseleave 鼠标离开
用event对象的button属性来区分鼠标按键:
- 0 左键
- 1 滚轮
- 2 右键
注意:能区分鼠标左右键的只有:mousedown 和 mouseup
DOM3标准规定:click事件只能监听左键。只能通过mousedown和mouseup来区分鼠标左右键。
事件的名称都是没有驼峰式的
写一个拖拽函数 drag(elem):
function drag(elem) { elem.addEventListener('mousedown', function (e) { e = e || window.event; var offsetX = e.offsetX; var offsetY = e.offsetY; document.onmousemove = function (e) { e = e || window.event; elem.style.left = e.pageX - offsetX + 'px'; elem.style.top = e.pageY - offsetY + 'px'; } e.stopPropagation(); },false); elem.addEventListener('mouseup',function () { document.onmousemove = null; },false); }
如果一个dom元素同时绑定了mousedown、mouseup、click共3个事件,它的执行顺序是:mousedown -> mouseup -> click
希望触发mousedown后,不触发click事件,怎么处理?
大概思路就是判断mousedown和mouseup之间的时间间隔是否满足一定值,根据这个时间间隔的大小来判断是拖拽还是点击。这里用上面的拖拽函数举个例子:(希望拖拽后不触发click事件)
其实这里有两种方案哈,一种是判断它是点击操作,然后给它绑定点击事件的处理函数,不是点击操作就清除事件,需要在拖拽函数里把click的处理函数传递过来,但需要考虑在函数外部绑定click事件方式,然后用同样的方式移除事件,比较麻烦。第二种方案是click事件在拖拽函数里绑定好,不在外部绑定。如果判断是点击事件,则执行里面的代码,否则不执行。显然第二种效率更好。故这里采取第二种方案:
function drag(elem, clickHandle) { var startTime,stopTime; elem.addEventListener('mousedown', function (e) { startTime = new Date().getTime(); e = e || window.event; var offsetX = e.offsetX; var offsetY = e.offsetY; document.onmousemove = function (e) { e = e || window.event; elem.style.left = e.pageX - offsetX + 'px'; elem.style.top = e.pageY - offsetY + 'px'; } e.stopPropagation(); },false); elem.addEventListener('mouseup',function () { stopTime = new Date().getTime(); document.onmousemove = null; },false); elem.addEventListener('click',function () { if (stopTime - startTime < 120) { clickHandle.call(); } },false); } var demo = document.getElementsByClassName("demo")[0]; drag(demo,function () { alert("点我干嘛呀"); })
补充知识点
IE浏览器上的方法:
- domEle.setCapture();
setCapture()函数执行之后,该dom元素会捕获页面上所有发生的事件,把全部事件揽到自己的身上。其他dom元素监听不到事件了。
- domEle.releaseCapture();
releaseCapture()函数执行后,该dom元素会释放之前捕获的事件。
移动端的几个事件:
- touchstart
- touchmove
- touchend
-
键盘事件
- keydown
- keyup
- keypress
执行顺序:keydown > keypress > keyup
keydown和keypress的区别:
- keydown可以响应任意键盘按键(除了Fn键),keypress只可以响应字符类键盘按键
- keypress可返回charcode(ASCII码),使用
String.fromCharCode()
可转换成相应字符
应用场景:大多数情况下,使用keydown就已经足够,如果需要区分按下的字符的大小写,需要使用keypress
-
文本操作事件
- input 监听文本的变化
- focus 文本框获取焦点
- blur 文本框失去焦点
- change 监听聚焦前和失去焦点后,文本是否发生变化
-
窗体操作类(window上的事件)
- scroll
应用:利用它可以模拟position: fixed定位
- load
当页面中所有元素全部加载完毕以后,load函数才会触发,因此它执行的顺序比较靠后。
网页中常用的字体颜色:#424242
JSON
JSON,即 JavaScript Object Notation(js对象表示法),是一种轻量级的数据传输格式。
JSON的本质就是一个JavaScript对象,在传输数据的时候,需要把该对象转换为字符串的形式。
当拿到一个JSON格式的字符串以后,可以把它转换成JSON对象进行操作。
- JSON.parse(); string -> JSON
- JSON.stringify; JSON -> string
异步加载
js加载的缺点:加载 js代码会阻塞文档,尽管有些 js 代码并不是操作文档的。过多的 js 文件加载会影响页面效率。
有些工具方法需要按需加载,用到的时候再加载,不用的时候不加载。
JavaScript异步加载的三种方案:
- defer 异步加载,需要等到DOM文档全部解析完才会执行。可加载script标签内的 js 代码。只有IE能用。
- async 异步加载,加载完毕立即执行。async只能加载外部脚本。
- 创建script标签,插入到DOM结构中,加载完毕后调用其中的方法。(此方式可实现按需加载)
封装一个兼容性函数,利用第三种方案,实现JavaScript异步加载:
function loadScript(url, callback) {
if (!url) return;
var script = document.createElement('script');
script.type = 'text/javascript';
if (script.readyState) {
script.onreadystatechange = function () {
if (script.readyState == 'complete' || script.readyState == 'loaded') {
callback();
}
}
} else {
script.onload = function () {
callback();
}
}
script.src = url;
document.head.appendChild(script);
}
用法:
loadScript('test.js',function () {
// 里面传递一个匿名回调函数,执行test.js里面的代码
test();
})
关于把函数传递到另一个函数中执行的方式思考:
- 匿名函数(callback)
- 传递一个字符串,执行时eval()
- 传递一个字符串,对象[callback]执行 ,此方式需要把要执行的函数封装到对象里。
浏览器加载时间线
1、创建Document对象,开始解析web页面。解析HTML元素和他们的文本内容后添加Element对象和Text节点到文档中。这个阶段document.readyState = ‘loading’。
2、遇到link外部css,创建线程加载,并继续解析文档。
3、遇到script外部js,并且没有设置async、defer,浏览器加载,并阻塞,等待js加载完成并执行该脚本,然后继续解析文档。
4、遇到script外部js,并且设置有async、defer,浏览器创建线程加载,并继续解析文档。对于async属性的脚本,脚本加载完成后立即执行。(异步加载 js 禁止使用document.write())
5、遇到img等,先正常解析dom结构,然后浏览器异步加载src,并继续解析文档。
6、当文档解析完成,document.readyState = ‘interactive’。
7、文档解析完成后,所有设置有defer的脚本会按照顺序执行。(注意与async的不同,但同样禁止使用document.write());
8、document对象触发DOMContentLoaded事件,这也标志着程序执行从同步脚本执行阶段,转化为事件驱动阶段。
9、当所有async的脚本加载完成并执行后、img等加载完成后,document.readyState = ‘complete’,window对象触发load事件。
10、从此,以异步响应方式处理用户输入、网络事件等。
正则表达式
转义字符
\
转义字符
\
可以用来书写多行字符串:var str = "hello\ world\ good\ morning";
windows系统中,一个回车表示
\r\n
linux系统中,一个回车就是
\n
此外,
\r
是行结束符;\t
是制表符,相当于一个Tab键的缩进距离,而缩进距离代表几个空格是由操作系统和具体的文本编辑器所决定的。
正则表达式(RegExp)的作用:匹配特殊字符或有特殊搭配原则的字符的最佳选择。
创建正则表达式的两种方式:
- 正则表达式字面量: var reg = /xxx/g
- 创建正则表达式对象:var reg = new RegExp(“xxx”,“g”)
细节方面:
使用new的方式,调用RegExp,参数传递一个字面量形式的reg,返回的对象与参数reg相互独立。
而直接调用RegExp函数,返回的正则表达式对象与参数reg是同一个引用。
修饰符:
修饰符 | 描述 |
---|---|
i | 执行对大小写不敏感的匹配 |
g | 执行全局匹配(查找所有匹配而非找到第一个匹配后停止) |
m | 执行多行匹配 |
这三个修饰符,或者叫正则表达式的三个属性,它们可以单独使用,也可以组合使用。
举例:多行匹配
var reg = /^a/gm;
var str = "abc\nabc";
str.match(reg); //["a", "a"]
方括号表达式:用于查找某个范围内的字符,例如;[a-z]、[^abc]
,在正则表达式中,方括号[]规定了字符串的某一位
字符的匹配规则。
写在方括号里的字符不会被转义
,|就是竖线,空格就是空格。方括号里会被转义的特殊符号只有 ^ 和 -
var reg = /a|b/g;
var str = "a|b";
str.match(reg); // ["a", "b"]
var reg = /[a|b]/g;
var str = "a|b";
str.match(reg); // ["a", "|", "b"]
另外,还有形如(blue|red|green)
的正则表达式:
var reg = /(blue|red|green)[\d]/g;
var str = "red1hello2green3blue4";
str.match(reg); //["red1", "green3", "blue4"]
元字符:拥有特殊含义的字符
元字符 | 描述 |
---|---|
. | 查找单个字符,除了换行和行结束符。 |
\w | 查找单词字符。 |
\W | 查找非单词字符。 |
\d | 查找数字。 |
\D | 查找非数字字符。 |
\s | 查找空白字符。 |
\S | 查找非空白字符。 |
\b | 匹配单词边界。 |
\B | 匹配非单词边界。 |
\n | 查找换行符。 |
\f | 查找换页符。 |
\r | 查找回车符(行结束符)。 |
\t | 查找制表符。 |
\v | 查找垂直制表符。 |
\xxx | 查找以八进制数 xxx 规定的字符。 |
\xdd | 查找以十六进制数 dd 规定的字符。 |
\uxxxx | 查找以十六进制数 xxxx 规定的 Unicode 字符。 |
元字符相当于方括号表达式的简写形式,它也同样匹配的是字符串的某一位。
有如下等量关系:
\w === [0-9A-z_]
\d === [0-9]
\s === [\t\n\r\v\f ] 后面有一位空格
. === [^\r\n]
量词
量词 | 描述 |
---|---|
n+ | 匹配任何包含至少一个 n 的字符串。 |
n* | 匹配任何包含零个或多个 n 的字符串。 |
n? | 匹配任何包含零个或一个 n 的字符串。 |
n{X} | 匹配包含 X 个 n 的序列的字符串。 |
n{X,Y} | 匹配包含 X 至 Y 个 n 的序列的字符串。 |
n{X,} | 匹配包含至少 X 个 n 的序列的字符串。 |
n$ | 匹配任何结尾为 n 的字符串。 |
^n | 匹配任何开头为 n 的字符串。 |
?=n | 匹配任何其后紧接指定字符串 n 的字符串。 |
?!n | 匹配任何其后没有紧接指定字符串 n 的字符串。 |
^ 放到 [] 里面代表非,放到表达式外 代表开头
写一个正则表达式,检验一个字符串首尾是否含有数字。
var reg = /^\d|\d$/g;
写一个正则表达式,检验一个字符串首尾是否都含有数字。
var reg = /^\d[\w\W]*\d$/g;
RegExp对象的几个属性
- global 是否具有具有修饰符g
- ignoreCase 是否具有修饰符i
- multiline 是否具有修饰符m
- lastIndex 一个整数,标识下一次匹配的字符位置
- source 正则表达式的源文本
RegExp对象的方法
- compile()
- exec()
- test()
重点说一下这个exec()方法
- 用该方法匹配字符串,会基于上一次匹配到的位置接着匹配。
- 匹配的位置完全由RegExp对象的lastIndex属性决定。
- 匹配结束以后,会从头开始匹配,即循环匹配。
- 如果是非全局匹配的正则表达式,调用exec()后lastIndex不会修改,所以不会基于上一次匹配的位置接着匹配。
var str = "abcabcabc";
var reg = /ab/g;
console.log(reg.lastIndex); // 0
console.log(reg.exec(str)); // ["ab", index: 0]
console.log(reg.lastIndex); // 2
console.log(reg.exec(str)); // ["ab", index: 3]
console.log(reg.lastIndex); // 5
console.log(reg.exec(str)); // ["ab", index: 6]
console.log(reg.lastIndex); // null
console.log(reg.exec(str)); //undefined
console.log(reg.lastIndex); // 0
console.log(reg.exec(str)); // ["ab", index: 0]
支持正则表达式的几个String对象的方法
- search()
- match()
- replace()
- split()
需求:匹配形如 "AAAA"的字符串。
这个时候,就需要借助我们的子表达式()和引用子表达式
var reg = /(\w)\1\1\1/g;
var str = "ghftddddgsjaaaa";
str.match(reg); //["dddd", "aaaa"]
其中子表达式必须用小括号括起来, \1
表示 引用第一个子表达式。
注意引用的不是匹配规则,引用的是匹配的结果
!
这样一来,匹配形如 “AABB” 的字符串也不在话下:
var reg = /(\w)\1(\w)\2/g;
var str = "gghhfsjifuuiifsf";
str.match(reg);//["gghh", "uuii"]
注意:使用exec()搭配子表达式执行的结果中,会包含匹配到的子表达式的结果:
var reg = /(\w)\1(\w)\2/g; var str = "gghhfsjifuuiifsf"; reg.exec(str);//["gghh", "g", "h", index: 0]
如果reg不加g,使用match()方法匹配,效果也是类似的:
var reg = /(\w)\1(\w)\2/; var str = "gghhfsjifuuiifsf"; str.match(reg);//["gghh", "g", "h", index: 0]
search() 检索第一次匹配到字符串的索引位置,如果匹配不到返回-1
需求:把匹配到的形如"AABB"的字符串全部替换成"BBAA"的字符串。
var reg = /(\w)\1(\w)\2/g;
var str = "hhbbuuii";
str.replace(reg,"$2$2$1$1"); //"bbhhiiuu"
其中,$1表示第一个子表达式匹配的结果,名字是固定的。
更灵活的情况,replace方法的第二个参数可以写一个函数:
var reg = /(\w)\1(\w)\2/g;
var str = "hhbbuuii";
str.replace(reg,function ($,$1,$2) {
return $2 + $2 + $1 + $1;
});
其中,$,即第一个参数表示整个表达式第一次匹配到的结果,$1表示第一个子表达第一次匹配到的结果。
例题:
把形如 "the-first-name"的字符串转换为小驼峰式的写法。
var reg = /-(\w)/g;
var str = "the-first-name";
str.replace(reg, function($,$1){
return $1.toUpperCase();
}) //"theFirstName"
正向预查/正向断言
// 匹配后面跟着b的a字符
var reg = /a(?=b)/g;
// 匹配后面不是跟着b的a字符
var reg = /a(?!b)/g;
后行断言
// 匹配前面跟着b的a字符
var reg = /(?<=b)a/g;
// 匹配前面不是数字的字母
var reg = /(?<!\d+)[a-z]+/g
打破贪婪匹配:在任何一个量词的后面加一个问号。
题目:字符串去重,把字符串"aaaaaaaaabbbbbbbbbbccccccccc"转换为字符串"abc":
var str = "aaaaaaaaabbbbbbbbbbccccccccc";
var reg = /(\w)\1+/g;
str.replace(reg,"$1");
题目:把字符串"100000000000"转换为"100 000 000"的形式:
var str = "100000000000";
var reg = /(?=(\B)(\d{3})+$)/g;
str.replace(reg," "); //"100 000 000 000"