前言:本篇文章原文为我在语雀上的学习笔记Javascript(1)——基础语法部分
web 发展史
Mosaic,是互联网历史上第一个获普遍使用和能够显示图片的网页浏览器。于 1993年问世。
1994 年 4 月,马克.安德森和 Silicon Graphics(简称为 SGI,中译为“视算科技”或“硅 图”)公司的创始人吉姆·克拉克(Jim Clark)在美国加州设立了“Mosaic Communication orporation”。
Mosaic 公司成立后,由于伊利诺伊大学拥有 Mosaic 的商标权,且伊利诺伊大学已将技术转让给 Spy Glass 公司,开发团队必须彻底重新撰写浏览器程式码,且浏览器名称更改为 Netscape Navigator,公司名字于 1994 年 11 月改名为“Netscape Communication Corporation”,此后沿用至今,中译为“网景”。微软的Internet Explorer及Mozilla Firefox等,其早期版本皆以Mosaic为基础而开发。微软随后买下Spy Glass公司的技术开发出Internet Explorer浏览器,而Mozilla Firefox则是网景通讯家开放源代码后所衍生出的版本。
js 历史
JavaScript 作为 Netscape Navigator 浏览器的一部分首次出现在 1996 年。它最初的设计目标是改善网页的用户体验。
作者:Brendan Eich
完成周期,十天,因此存在很多问题和bug
期初 JavaScript 被命名为 LiveScript,后因和 Sun 公司合作,因市场宣传需要改名JavaScript。后来 Sun 公司被 Oracle 收购,JavaScript 版权归 Oracle 所有。
浏览器组成
- shell 部分——用户能操作部分(壳)
- 内核部分——用户看不到的部分
-
- 渲染引擎(语法规则和渲染)
- js 引擎
- 其他模块(如异步)
JS引擎
- 2001 年发布 ie6,首次实现对 js 引擎的优化。
- 2008 年 Google 发布最新浏览器 Chrome,它是采用优化后的 javascript 引擎,引擎代号 V8,因能把 js 代码直接转化为机械码来执行,进而以速度快而闻名。
- 后 Firefox 也推出了具备强大功能的 js 引擎
- Firefox3.5 TraceMonkey(对频繁执行的代码做了路径优化)
- Firefox4.0 JeagerMonkey
js 的逼格(特有特色)
js 是解释性语言:(不需要编译成文件)跨平台
单线程:同一时间只能做一件事——js 引擎是单线程(同一时间做很多事叫多线程)
ECMA(欧洲计算机制造联合会)标注:为了取得技术优势,微软推出了 JScript,
CEnvi 推出 ScriptEase,与 JavaScript 同样可在浏览器上运行。为了统一规格 JavaScript
兼容于 ECMA 标准,因此也称为 ECMAScript。
js 三大部分 ECMAScript、DOM、BOM
如何引入 js?
- 页面内嵌<script></script>标签,写 head 里面也行,写 body 里面也行
- 外部 js 文件,引入<script src=“location”></script>
- 一个文件中可以包括多个 css,js——不混用
- 特殊写页面,大部分写在外部——不混用
- 如果同时写了内部的 js 和外部的 js,那么是外部的 js 文件显示出来
JS基本语法
HTML,CSS 不是编程语言,是计算机语言,编程语言需要有变量和函数。变量是存放东西,方便后续使用
变量申明
var a; 这个叫变量声明。我们向系统中申请了 var 这个框,命名叫 a
给 a 赋值 100,写作 a =100,这里不是等号是赋值
var a ;a =100;可以简化写成 var a=100;
命名规则
(用接近的英文单词)———— 起变量名一定要以英文语义化
- 变量名必须以英文字母、_、$ 开头
- 变量名可以包括英文字母、_、$、数字
- 不可以用系统的关键字、保留字作为变量名
基本语法
下面是变量,例:
var a = 10;
var b = 20;
var c;
c = a + b;
先运算等号右边的 a+b,运算完后,再赋值给左边 c
先取值,再赋值
运算大于赋值的优先级
js 是动态语言,动态语言基本上都是解释性语言,解释性语言基本上都是脚本语言
js 是浮点型语言(带小数点)
数据类型(值类型)
1、不可改变的原始值(栈数据)栈 stack
Number,String,Boolean,undefined,null
已经放进去的值不可改变,只会改房间编号为 null(硬盘原理)
Number 数字,例 var a = 123;
String 字符串,语言放双引号里,例 var a="语言",""是空串
Boolean 布尔数字,就两个值,false,true
undefined 是没有定义的,表示还没赋值,仅一个值 underfined
null 代表空,占位用,用空值来覆盖
var a =10;
var b = a;
a = 20;
document.write(b); // 10
原始值是我把一个值放到另一个值里面,改了第一个值,第二个值不变
2、引用值(堆数据)大致上放堆 heap 里面
array 数组, Object, function ... data,RegExp 正则
var arr = [1];
var arr1 = arr;
arr.push(2);
document.write(arr1);
// arr 是 1,2。arr1 是 1,2
var arr = [1,2];var arr1 =arr;arr.push(3);
这往[1,2]放 3,arr 和 arr1 都是[1,2,3]
引用值是在栈内存里面放堆的地址,拷贝的也是地址,所以改变 arr,arr1 也变了
引用值是把第一个值放到第二个值里面,改第一个值,第二个值也改变
js 由值决定类型。原始值和引用值唯一的不同是赋值形式不同
拓展知识:JS垃圾回收:
JS引擎会定期的发现内存中无法访问到的对象,该对象称之为垃圾对象,JS引擎会在合适的时间将其占用的内存释放
运算符相关知识点
操作符:运算符,参与运算的符号
操作数:参与运算的数据,也称之为源
操作符不一定只有一个符号 like --》 () ++ --
操作符出现在不同的位置,可能有不同的涵义
按照操作数区分
一元(目)运算符 : () .
二元(目)运算符 : + - / * % =
三元(目)运算符 : ?:
按照功能区分
1.算术运算符(数学)
- + - * / (加减乘除)
- + - (表示正负)
- % ( 取余)
- ++ -- ( 自增自减)
- ** (幂运算)
加减乘除
加号
- “+”作用:数学运算、字符串链接
- 任何数据类型加字符串都等于字符串
var a = “a”+ true + 1; //打印 atrue1
var a = 1 + “a” + 1 + 1; //打印 1a11
var a = 1 + 1 + “a” + 1 + 1; //打印 2a11,从左向右运算
var a = 1 + 1 + “a” +( 1 + 2); //打印 2a3
减号,乘号,除号
var a = 0 – 1; //等于-1
var a = 2 * 1; //等于 2
var a = 0 / 0; //答案是 NaN,应该得出一个数字类型的数,但是没法表达,就用 NaN (NaN 是 Not a Number 非数,不是数,但是是数字类型
var a = 1 / 0; //是 infinity
var a = -1 / 0; //是-infinity
tips : 小数的运算,在计算机内是不精确的
正负
没什么好讲
%,摩尔,模,是取余数的意思
var a =5%2 //5%2 是五除二的余数,商二余一
var a =5%1 //是五除一的余数,结果是 0
var num = 1 % 5; //意思是 1 除以 5 的余数。商 0 余 1
var a =4%6 //是四除六的余数,结果是 4
var a = 4;a % = 5;document.write(a); // 4
var a = 0;a % = 5;document.write(a); //0
var a = 10;a %= 2;document.write(a); //0
var a = 3;a % = 4; //4
取模的运算结果和被除数有关
求模和求余还是有一些区别的
“++”,“- -”,”+=“,“-=”,“/=“,“*=”,“%=”
++
var a = 10; a = a + 1; //结果 11
var a =10;
document.write(++a ); //答案 11;11是先执行++,再执行本条语句 document.write(++a)
var a =1;
document.write(a ++);document.write(a); //答案 1;2。是先执行语句(document.write(a)),再++,所以第一次打印的还是 a,第二次打印 a++后的值
var a =10;
var b=++a -1+a++;
document.write(b + “ ” + a); //答案 21 12先++a,这个时候 a=11,再-1,再加 a,b 就是 21,最后++,a 就是 12
var a =1;var b = a ++ + 1;
document.write(b); //答案 2,先执行 var b =a+1, 再 a++
var a =1;var b = a ++ + 1;
document.write(a);document.write(b); //答案 2,2
var a =1;var b = ++a + 1;
document.write(a);document.write(b); //答案 2,3
var i = 1;var a = i++; //答案 a = 1; 此时 i 先将值 1 赋给 a,然后自己+1,i=2;
var i = 2;
var b = ++i; //答案 b = 3;此时 i 先自己+1 为 3.再给 b 赋值,b=3;
var x = 1;
var y = x + x++ * (x = x + x++ * ++x) + x; // 21
赋值的顺序自右向左,计算的顺序自左向右(按数学来)
--
“- -”,是自身减一,在赋值给自身
var a = 1;var b = a-- + -- a;
document.write(b); //答案 0,先执行--a;此时 a 变成0,然后第一个 a 也变成 0,那么 b = 0-- + --a
var a = 1;var b = --a + --a;
document.write(b); //答案-1
var a = 1;
document.write(a++);document.write(a); //答案 1;2
var a = 1;
document.write(++a);document.write(a); //答案 2;2
var a =1;
var b = a ++ +1;
document.write(b); //答案 2 a 写在后面就后运行,先计算 a+1=2 赋值给 b 后再++
var a = 1;var b= ++a + 1;
document.write(a);document.write(b); //答案 2;3
+= -=
var a =10;a ++;a ++;a ++; // 想要加十个
// 简化写法:a +=10;也是 a = a+10;
var a =10;a += 10 + 1; // 答案 21
var a = 1;a = a + 10; // 等于 a+=10
// a++是 a +=1 的写法
/=
var a=10;a/=2; //答案 5,是除二赋给自身的意思
*/
var a =10;a *=2; //答案:20,是乘二赋给自身的意思
%=
var a=10;a%=2; //答案:0, 10 能整除 2,余数是 0,取余,余数赋给自身。
var a=3;a%=4; //答案:3,3 除以 4,余数为 3,余数赋给自身。
var a=0;a%=4; //答案:0,0 除以 4,余数为 0,余数赋给自身。
var a = 1;a% =10; //答案:1,1 除以 10,余数为 1,余数赋给自身。
幂运算
5**2; // 25
5**3; //125
-5**3; // 报错,因为- 和 ** 的优先级都很高 容易产生歧义
2.比较运算符
“>”,”<”,”==”,“>=”,“<=”,“!=”,“===”,“!==”比较结果为 boolean 值
字符串的比较,比的是 ASCII 码(七位二进制 0000000)
>, <
var a = "a">"b";document.write(a); // 答案是 false
var a = 1 > 2;document.write(a); // 答案是 false
var a = 1 < 2;document.write(a); // 答案是 true
var a = "1">"8";document.write(a); // 答案是 false
var a = "10">"8";document.write(a); // 答案 false,不是十和八比,是字符串一零和八比,先用开头的一和八比,比不过就不看第二位了;一样的就拿零和八比
var a = '1' > 10; // false 如果一个不是字符串,且两个都是原始值,将他们都转换成数字进行比较
// 运算结果为真实的值
==,等不等于
var a = 1 == 2; //答案是说 1 等不等于 2,因为 1 肯定不等于 2,所以值为 false
var a = NaN == NaN; //答案是 false,NaN 不等于任何东西,包括他自己
var a = undefined == underfined; //答案是 true
var a = infinity == infinity; //答案是 true
var a = NaN == NaN; //答案是 false。非数 NaN 是不等于自己的
// NaN 得不出数,又是数字类型,就是 NaN
>=,<=
!=是否不等于,非等
比较结果为 boolean 值:true 和 false
3.逻辑运算符
“&&”,“||”,“!“运算结果为真实的值
“&&”与运算符
两个表达式:先看第一个表达式转换成布尔值的结果是否为真,如果结果为真,那么它会看第二个表达式转换为布尔值的结果,然后如果只有两个表达式的话,只看看第二个表达式,就可以返回该表达式的值了,如果第一位布尔值为 false,不看后面的,返回第一个表达式的值就可以了
var a = 1 && 2; //答案 2,如果第一位 1 为真,结果就为第二位的值 2
var a = 1 && 2 + 2; //答案 4,如果 1 为真,结果就为 4
var a = 0 && 2 + 2; //答案 0
var a = 1 && 1 && 8; //答案 8,先看第一个是否为真,为真再看第二个,中途如果遇到 false,那就返回 false 的值
var a =1 + 1 && 1 – 1;document.write(a); //答案 0
如果是三个或多个表达式,会先看第一个表达式是否为真,如果为真,就看第二个表达式,如果第二个也为真,就看第三个表达式(如果为真就往后看,一旦遇到假就返回到假的值),如果第三个是最后一个表达式,那就直接返回第三个的结果
如果第一个是假,就返回第一个值,当是真的时候就往后走,一旦遇到假,就返回
2>1 && document.write('榆哥很帅') //意思是如果 2 大于 1,那么就打 印榆哥很帅,如果前面真才能执行后面的(相当于短路语句使用)
“||”或运算符
var num = 1 || 3; //答案 1
var num = 0 || 3; //答案 3
var num = 0 || false; //答案是 false
var num = 0 || false || 1; // 1
看第一个表达式是否为真,如果为真,则返回第一个值,碰到真就返回如果第一个表达式是假,就看第二个表达式,如果第二个是最后一个,就返回第二个的值
关注真假的说法:全假才为假,有一个真就为真
“!“非运算符,否的意思。
先转成布尔值,再取反
var a = ! 123;document.write(a); //答案 false。123 的布尔值是 true,取反是 false
var a = ! “”;document.write(a); //答案 true。空串””布尔值是 false,取反是 true
var a = ! !“”;document.write(a); //答案 false,取反后,再反过来,结果不变
var a = true;a =!a;document.write(a) //答案 false,自身取反,再赋值给自身
被认定为 false 的值:转换为布尔值会被认定为 false 的值 undefined,null,NaN,“”(空串), 0, false
4.位运算符
(这里我使用我之前写的博客来讲解)
位运算概述
从现代计算机中所有的数据二进制的形式存储在设备中。即0、1两种状态,计算机对二进制数据进行的运算(+、-、*、/)都是叫位运算,即将符号位共同参与运算的运算。
位运算概览
符号 | 描述 | 运算规则 |
---|---|---|
& | 与 | 两个位都为1时,结果才为1 |
| | 或 | 两个位都为1时,结果才为1 |
^ | 异或 | 两个位相同为0,相异为1 |
~ | 取反 | 0变1,1变0 |
<< | 左移 | 各二进位全部左移若干位,高位丢弃,低位补0 |
>> | 右移 | 各二进位全部右移若干位,对无符号数,高位补0,有符号数,各编译器处理方法不一样,有的补符号位(算术右移),有的补0(逻辑右移) |
位运算预算规则详细描述
1)按位与运算符(&)
定义:参加运算的两个数据,按二进制位进行“与”运算。
var v1 = 1 & 1; //二级制表示 0001 & 0001 = 0001 = 1
var v2 = 3 & 4; //二级制表示 0011 & 0100 = 0000 = 0
var v3 = 6 & 7; //二级制表示 0110 & 0111 = 0110 = 6
var v4 = 18 & 24;//二级制表示 00010010 & 00011000 = 00010000 = 16
2)按位或运算符(|)
定义:参加运算的两个对象,按二进制位进行“或”运算。
var v1 = 1 | 1; //二级制表示 0001 | 0001 = 0001 = 1
var v2 = 3 | 4; //二级制表示 0011 | 0100 = 0111 = 7
var v3 = 6 | 7; //二级制表示 0110 | 0111 = 0111 = 7
var v4 = 18 | 24;//二级制表示 00010010 | 00011000 = 00011010 = 26
3)异或运算符(^)
定义:参加运算的两个数据,按二进制位进行“异或”运算。
先大致讲下什么是异或:
异或(xor)是一个数学运算符。它应用于逻辑运算。异或的数学符号为“⊕”,计算机符号为“xor”。其运算法则为:
a⊕b = (¬a ∧ b) ∨ (a ∧¬b)
如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。
var v1 = 1 ^ 1; //二级制表示 0001 | 0001 = 0000 = 0
var v2 = 3 ^ 4; //二级制表示 0011 | 0100 = 0111 = 7
var v3 = 6 ^ 7; //二级制表示 0110 | 0111 = 0001 = 1
var v4 = 18 ^ 24;//二级制表示 00010010 | 00011000 = 00001010 = 10
4) 取反运算符 (~)
定义:参加运算的一个数据,按二进制进行“取反”运算。
~1=0
~0=1
5) 左移运算符(<<)
定义:将一个运算对象的各二进制位全部左移若干位(左边的二进制位丢弃,右边补0)
var v1 = 1 << 3;; //二级制表示 0001 = 1000 = 8
var v2 = 3 << 2;; //二级制表示 0011 = 1100 = 12
var v3 = 6 << 5; //二级制表示 00000110 = 11000000 = 192
var v4 = 18 << 1; //二级制表示 00010010 = 00100100 = 36
6)右移运算符(>>)
定义:将一个数的各二进制位全部右移若干位,正数左补0,负数左补1,右边丢弃。
var v1 = 12 >> 3;; //二级制表示 1100 = 0001 = 1
var v2 = 13 >> 2;; //二级制表示 1101 = 0011 = 3
var v3 = 56 >> 4; //二级制表示 00111000 = 00000011 = 3
var v4 = 18 >> 1; //二级制表示 00010010 = 00001001 = 9
讲了位运算的概述,要明白位运算是在二进制中的运算方式,所有其他进制的数在进行位运算时都要先转化成二进制数再进行运算。所以,位运算是一种十分高效的运算,无论是嵌入式编程还是优化系统的核心代码,适当的运用位运算总是一种迷人的手段。如果能把位运算运用的神出鬼没,很多程序都将十分精妙。
那么今天我们就来看看位运算能进行哪些应用吧。
1.判断某个数的奇偶
相信大多数人第一思维就是
if (n % 2 == 1){
console.log('odd');
}else{
console.log('even');
}
这么写,对!当然对,没有人说你错,但是,我们竟然掌握了位运算,为什么不用时间效率更加高效的方法呢?
if (n & 2){
console.log('odd');
}else{
console.log('even');
}
哪种高效,自己可以去试试咯
2.m的n次方
第一反应,pow(),嘿嘿,当然也对,如果不让你用这个函数呢?
累乘呗! 我想,不只是你,小学生都知道,时间复杂度为O(n)。
来吧,来个更高效的位运算大法:时间复杂度近为 O(logn)
//求3的11次
var m = 3, n = 11; //11的二进制为 1011
var sum = 1; //m^11 = m^1000 * m^0010 * m^0001;
while (n != 0)
{
if (n & 1 == 1){
sum *= m;
} //判断最后一位是不是1 是的话就乘以
m *= m;
n = n >> 1; //右移一位
}
console.log(sum);
3.交换两个数(不借助第三个变量)
不借助第三个变量!
a=a+b; b=a-b; a=a-b;
嘿嘿 当然,这种也行,来看咱们的位运算大法吧:
var a, b;
a ^= b;
a ^= b ^= a;
console.log(a, b);
看不懂? 没事 看下面这个
var a, b; //a = 3 = 0011 b = 4 = 0100
a = a ^ b; //a = 0011 ^ 0100 = 0111
b = a ^ b; //b = 0111 ^ 0100 = 0011
a = a ^ b; //a = 0111 ^ 0011 = 0100
console.log(a, b);
嘿嘿是不是很有逼格
4.乘/除2的值
var n;
n<<1; //n乘2
n>>1; //n除2
5.判断两个数正负性是否相同
var x, y;
if ((x ^ y) >= 0){
console.log('identical');
}else{
console.log('inequality');
}
6.判断是否为2的整数幂
var a = 64 & 63; // 01000000 & 00111111 = 0 64是2的整数幂
var b = 8 & 7; // 1000 & 0111 = 0 8是2的整数幂
var c = 7 & 6; // 0111 & 0110 = 6 7不是2的整数幂
var n; // 待测数
if (n & (n - 1)){
console.log('NO');
}else{
console.log('YES');
}
7.找出不大于n的最大的2的整数幂
var n;
for (var i = n; i >= 0; i--)
{
if (!(i & (i - 1))){
console.log(i);
break;
}
}
5.其他
逗号运算符等
表达式
表达式 = 操作符 + 操作数
每个表达式都有它的运算结果,该结果叫返回值,返回值类型叫做返回类型
所有的表达式都已当作数据来使用
目前学习的运算符和返回值和类型
1. " = " : 该表达式 返回赋值结果
举例:
console.log(a = 1); // 输出1
var a, b, c, d;
a = b = c = d = 1;
// a,b,c,d 均为1
2. " . " :属性访问表达式,返回的是属性的值
不举例了!
3. " [] " : 属性访问表达式,返回的是属性的值
不举例了!
4. " () " :函数调用表达式,返回的结果取决于函数的执行
举例:
console.log(console.log());// 返回结果为undefined
5.如果是一个申明 + 赋值的 表达式,返回结果为undefined
拓展知识:Chrome 浏览器控制台
chrome 浏览器的控制台环境是REPL环境
REPL:Read-Eval-Print-Loop 读-运行-输出-循环
当直接在控制台书写代码的时候,除了运行代码之外,还会输出该表达式的返回值
具体补充变量 数据类型 类型转换
变量
JavaScript的变量是松散类型的,所谓松散类型就是可以用来保存任何类型的数据。换句话说,每个变量仅仅是一个用于保存值的占位符而已。定义变量时要使用 var 操作符(注意 var 是一个关键字),后跟变量名(即一个标识符)
var a;//定义了一个名为 a 的变量
JavaScript 也支持直接初始化变量,因此在定义变量的同时就可以设置变量的值,如下所示:
var a = "hi";
在此,变量 a 中保存了一个字符串值"hi"。像这样初始化变量并不会把它标记为字符串类型;初始化的过程就是给变量赋一个值那么简单。因此,可以在修改变量值的同时修改值的类型,如下所示:
var a = "hi";
a = 100; // 有效,但不推荐
数据类型
JavaScript 中有 5 种简单数据类型(也称为基本数据类型):Undefined、Null、Boolean、Number和 String。还有 1种复杂数据类型——Object,Object 本质上是由一组无序的名值对组成的。JavaScript不支持任何创建自定义类型的机制,而所有值最终都将是上述 6 种数据类型之一。
undefined 派生于 null 因此用 “==”来比较时候 会返回true
typeof操作符
鉴于 JavaScript 是松散类型的,因此需要有一种手段来检测给定变量的数据类型——typeof 就是负责提供这方面信息的操作符
console.log(typeof("123"));
console.log(typeof("Hello"));
console.log(typeof("false"));
//均为string
console.log(typeof(10));
console.log(typeof(10.24));
console.log(typeof(NaN));
console.log(typeof(Infinity));
//均为number
console.log(typeof(true));
console.log(typeof(false));
//均为boolean
console.log(typeof( [] ));
console.log(typeof( {} ));
console.log(typeof( null ));
//均为object
console.log(typeof( function(){} ));
//为function
console.log(typeof( ID ));
var a;
console.log(typeof( a ));
//均为undefined
控制台输出
注意:
typeof 是一个操作符而不是函数,因此例子中的圆括号尽管可以使用,但不是必需的。
下面两段代码是等价的
console.log(typeof("123"));
console.log(typeof "123" );
显类型转换
Number(mix)
parseInt(string,radix)
parseFloat(string)
toString(radix)
String(mix)
Boolean()
Number
var a = Number("123");
console.log(typeof(a) + " : " + a);
var b = Number(true);
console.log(typeof(b) + " : " + b);
var c = Number(false);
console.log(typeof(c) + " : " + c);
var d = Number(null);
console.log(typeof(d) + " : " + d);
var e = Number(undefined);
console.log(typeof(e) + " : " + e);
var f = Number("abc");
console.log(typeof(f) + " : " + f);
var g = Number("123abc");
console.log(typeof(g) + " : " + g);
控制台输出
Number()显式类型转换,必定会转换成number类型,而不能转化成数字的,就转化成NaN(Not a Number)
这里几个转化成NaN的例子需要记忆一下
parseInt
无基底
即parseInt(string)
console.log(parseInt(123));
console.log(parseInt(123.9));
console.log(parseInt("123"));
console.log(parseInt("123.5"));
console.log(parseInt("123.5.9"));
console.log(parseInt("abc"));
console.log(parseInt("123.8abc"));
console.log(parseInt(true));
console.log(parseInt(false));
console.log(parseInt(undefined));
输出
含基底
即parseInt(string,radix)
(radix 的 范围一般是 2~36)
把string 按照 radix 表示的进制为基底,转化成10进制整数
console.log(parseInt(10,16));
console.log(parseInt("A",16));
console.log(parseInt(1000,2));
控制台输出
parseFloat
parseFloat(string) 就比较简单,就不做解释了
String
String这个函数耶比较简单,也就不做解释了
console.log(String(16) + " : " + typeof(String(16)));
console.log(String(true) + " : " + typeof(String(true)));
console.log(String(undefined) + " : " + typeof(String(undefined)));
console.log(String(null) + " : " + typeof(String(null)));
控制台输出
Boolean
记住下面7个,除了这 7个 其他值通过 Boolean转化都是true
console.log(Boolean(""));
console.log(Boolean(null));
console.log(Boolean(undefined));
console.log(Boolean(0));
console.log(Boolean(-0));
console.log(Boolean(false));
console.log(Boolean(NaN));
控制台输出
toString
toString(radix) 也是想把变量转化成字符串
var demo = 123;
var a = demo.toString();
console.log(a + " : " + typeof(a));
这点和String 转化比较类似,但使用方法不同
这里介绍下不同之处
toString(radix) 含有radix 可以作为基底,
将 待转化的数 转化成 radix 进制的数以string来保存
它的使用方法如下
var demo1 = 16;
var a = demo1.toString(2);
console.log(a + " : " + typeof(a));
//将16 转化成二进制数
var demo2 = 17;
var b = demo2.toString(16);
console.log(b + " : " + typeof(b));
//17转化成16进制数
var demo3 = 15;
var c = demo3.toString(16);
console.log(c + " : " + typeof(c));
//15转化成16进制数
控制台输出如下
同时,null 和 undefined 是不能使用toString的
来一题看看
console.log(typeof(a)); //输出undefined
console.log(typeof(typeof(a))); //输出为什么?
答案是 string
隐式类型转换
isNaN()
这个看看就懂了,其中内部进行了Number()的调用
console.log(isNaN(undefined));
console.log(isNaN(NaN));
console.log(isNaN("a"));
console.log(isNaN(null));
console.log(isNaN(false));
onsole.log(isNaN(true));
console.log(isNaN(123));
console.log(isNaN("123"));
"++""- -""+""-"
加加,减减,一元正负运算
内部调用也是Number()
var a = "123";
a ++;
console.log(typeof(a) + " : " + a);
var b = "abc";
b --;
console.log(typeof(b) + " : " + b);
var c = null;
c ++;
console.log(typeof(c) + " : " + c);
var d = "a";
d = +d;
console.log(typeof(d) + " : " + d);
var e = "123";
e = -e;
console.log(typeof(e) + " : " + e);
“+”加号
加号有一点比较特殊,当加号两边有一侧是字符串的话,那么表达式得到的就是字符串,也就是隐式调用了String()
var a = "a" + 1;
console.log(typeof(a) + " : " + a);
var b = 1 + "";
console.log(typeof(b) + " : " + b);
var c = null + "abc";
console.log(typeof(c) + " : " + c);
+"Infinity" 返回 Infinity
+"1/3" NaN 无法识别字符串内部的表达式
“-” “*” “/” “%”
减号,乘号,除号,mod,他们隐式调用的也是Number()
var a = "a" * 1;
console.log(typeof(a) + " : " + a);
var b = 1 - "";
console.log(typeof(b) + " : " + b);
var c = "2" / null;
console.log(typeof(c) + " : " + c);
var d = "1" - 1;
console.log(typeof(d) + " : " + d);
&& || ! if()
与, 或, 非 ,if() 条件判断,均是调用给了Boolean()
这里就不做演示了
">" "<" ">=" "<="
比较大小的判断,若比较符两边有一个是数字,就会进行Number转化,同时返回boolean类型的值
var a = "a" > 1;
console.log(typeof(a) + " : " + a);
var b = 1 < "";
console.log(typeof(b) + " : " + b);
var c = "2" > null;
console.log(typeof(c) + " : " + c);
var d = "1" > 1;
console.log(typeof(d) + " : " + d);
"==" "!="
var a = "1" == 1;
console.log(typeof(a) + " : " + a);
var b = 0 == "";
console.log(typeof(b) + " : " + b);
var c = 0 != null;
console.log(typeof(c) + " : " + c);
var d = 0 == false;
console.log(typeof(d) + " : " + d);
补充最特殊的:
null == undefined 结果为true
null === undefined 结果为false
NaN == NaN 结果为false
最后,再来给你们看看一个东西吧
这个是为什么,就请自己思考了
流程控制
If 语句 if、if else if if <—> && 转换
switch case
for循环
while循环
do while
不讲了
初识引用值
数组
用于存放多个数据
创建方式:
new Array(数组长度)
长度指的是,数组里面的数据总数,长度一定是非负整数
new Array(数据, 数据, 数据, ···)
创建一个数组,并初始化每一项的值
数组项:数组其中的一项数据
[数据,数据,数据]
创建一个数组,并初始化每一项的值
认识数组的本质
数组的本质是对象
对象
var obj = {
里面存属性和方法
key 属性名:value 属性值;
}
在{}面用。属性与属性之间用逗号隔开
函数
以上情况就是偶合,偶合度非常高,偶合代码就是低效代码
编程讲究高内聚,弱偶合
下面是简便写法:可以用 test 调用执行,写几个 test 就调用执行几次
一、定义
1、函数声明
定义一个函数可以先写一个 function,函数就是另一个类型的变量
我声明一个函数 test,test 是函数名。写成下面
function test(){
函数体
}
函数名起名:开发规范要求,函数名和变量名如果由多个单词拼接,必须符合小驼峰原则(第一个单词首字母小写,后面的首字母大写)
function theFirstName(){}
document.write(theFirstName);
答案 function theFirstName(){}。打印出来的是函数体
这与 c 语言和 c++,他们打印指针,会输出指针的地址,而 js 这种弱数据语言(解释性语言)永远不输出地址,输出地址指向房间
2、函数表达式
(1)命名函数表达式
var test = fuction test (){
document.write(‘a’);
}
test();
这种方式像定义一个变量
上面这种方式,可以演变成第三种,匿名表达式【不写 test 这种函数名】
(2)匿名函数表达式(常用,一般说的函数表达式就是匿名函数表达式)
var demo = fuction (){
document.write(‘a’);
}
demo();
二、组成形式
1、函数名称
function test(){}其中 function 是函数关键字,test 是函数名,必须有(){},参数可有可没有,参数是写在()括号里面的。
如果写成 function test(a,b){},相当于隐式的在函数里面 var a,var b 申明了两个变量,()括号里面不能直接写 var
function test(a, b){
document.write(a + b)
}
test(1, 2);
2、参数(可有可没有,但是高级编程必有)
(1)形参(形式参数):指的是 function sum(a,b){}括号里面的 a 和 b
(2)实参(实际参数):指的是 sum(1,2);里面的 1,2
天生不定参,形参可以比实参多,实参也可以比形参多
function test(a, b, c,d) {
document.write(a);
document.write(d);
}
sum(11, 2, 3); // 11 , undefined
答案 11,undefined,上面这道题是形参多,实参少
js 参数不限制位置,天生不定参数
在每一个函数里面都有一个隐式的东西 arguments 这个是实参列表
function test(a) {
console.log(arguments);
console.log(arguments.length); // 实参长度
}
sum(11, 2, 3);// [11, 2, 3],3
function test(a) {
for(var i = 0; i < argument.length; i++){
console.log(arguments[i]);
}
}
sum(11, 2, 3); // 11,2,3
// 形参长度求法
function sum(a, b, c, d) {
console.log(sum.length);
}
sum(11, 2, 3); // 4
// 例任意个数求和(不定参才能求出来)
// 形参永远有尽头,要实现任意的数求和,无法定义形参。
function sum() {
var result = 0;
for (var i = 0; i < arguments.length; i++) {
result += arguments[i];
}
console.log(result);
}
sum(1, 2, 3, 4, 5, 6, 7, 8);
function sum(a, b) {
arguments[0] = 3;
console.log(a);
}
sum(1, 2); // 3
function sum(a, b) {
a = 4
console.log(arguments[0]);
}
sum(1, 2); // 4
function sum(a, b) {
b = 4;
console.log(arguments[1]);
}
sum(1); // undefined arguments是和实参列表相映射
3、返回值 return
结束条件和返回值 return,return 有终止函数的功能
没写 return,实际上是加上了一个隐式的 return
function sum(a, b){
console.log('a');
console.log('b');
return;
}
// 答案 a,b
function sum(a, b){
console.log('a');
return;
console.log('b');
}
// 答案 a
return 最常用的是返回值。本意是把一个值返回到函数以外
function sum(){
return 123;
console.log('a');
}
var num = sum(); // num = 123
// 而且 console.log(‘a’);无效,这里的 return 又终止函数,又返回变量
function myNumber(target){
return +target; // 利用+隐式的转换成了数字类型
}
var num = myNumber('123');
console.log(typeof(num) + ' ' + num); // number 123
作用域初探
作用域定义:变量(变量作用于又称上下文)和函数生效(能被访问)的区域全局、局部变量
作用域的访问顺序
作用域:函数里面的可以访问外面的全局变量
var a = 123;
function test() {
var b = 123;
function demo() {
var c = 234;
console.log(b);
console.log(c);
}
demo();
console.log(c);
}
test();
// 123 234 c is not defined
函数外面不能用函数里面的。里面的可以访问外面的,外面的不能访问里面的,彼此独立的区间不能相互访问
彼此独立的区间不能相互访问.全局变量都可以访问
var a = 123;
function demo() {
var b = 123;
}
function test() {
var c = 234;
}
test{}和 demo{}不能互相访问,但是可以访问外面的全局变量
预编译
js 运行三部曲
1 语法分析 → 2 预编译 → 3 解释执行
预编译前奏
function test(){
console.log(‘a’);
}
test();
/* ——————————————————————————————————————————————————————————————————————————*/
// 上面能执行
test();
function test(){
console.log(‘a’);
}
// 也能执行,因为有预编译的存在
/* ——————————————————————————————————————————————————————————————————————————*/
var a = 123;
console.log(a); // 答案 123
console.log(a);
var a = 123; // undefined
- 函数声明整体提升:函数不管写到哪里,都会被提到逻辑的最前面。所以不管在哪里调用,本质上都是在后面调用
- 变量 声明提升:把 var a 提升到最前面 var a = 123;这是变量声明再赋值。
imply global 暗示全局变量
- imply global 暗示全局变量:即任何变量,如果变量未经申明就赋值,此变量就为全局对象所有
- 一切申明的全局变量全是window的属性
我直接用代码来展示吧
//console.log(a); 单独这么一条语句肯定报错
a = 10;
console.log(a);
下面两句并未报错
同时控制台上输入window.a,输出的是10
我们说了,任何变量,如果变量未经申明就赋值,此变量就为全局对象所有,至于这个window就是全局对象
//上述代码可以这么理解
window{
a : 10;
}
//未申明的变量a赋值,则相当于为window对象增加了一个叫a的属性,属性值为10
一切申明的全局变量全是window的属性
这条就简单了,在全局范围内申明的变量,都是window的属性
var a = 111;
-->window.a = 111;
console.log(a) ==> console.log(window.a)
再来看一段代码
function fn() {
var a = b = 10;
}
fn();
console.log(window.b);
赋值的顺序是从右往左,所以顺序是:
- b = 10
- var a = b
那么对于b来说,b是一个未声明的变量,对他进行了赋值,那么它就属于全局对象window里的属性了
那么这一点就全都搞懂了
预编译详解
关于预编译,我们一定听过很多人说的
- 函数声明整体提升
- 变量 声明提升
这两句话,很好理解,虽然能解决大多数问题,但是,并不是我所愿的,就像下面这个问题,他并不能解决。
先来看一段代码
试着说出其输出结果
function f(a) {
console.log(a);
var a = 10;
console.log(a);
function a() {}
console.log(a);
var b = function () {}
console.log(b);
}
f(1);
我们通过这段代码来讲解预编译的相关知识
要解决这个问题,我们肯定要清楚,谁先提升到谁前面,谁又要覆盖谁
下面就是预编译的重点:
- 创建AO对象
- 找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
- 将实参值和形参统一
- 在函数体里面找函数声明,值赋予函数体
我们按照步骤来
创建AO对象(Activation Object 执行期上下文)
AO{
}
找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
AO{
a : undefined, //a先是传入了形参,之后因为有一个变量 var a 所以现在传入的是 var a
b : undefined; //函数表达式也是申明的
//将其均赋值 undefined
}
将实参值和形参统一
AO{
a : 1, //a先是传入了形参,之后因为有一个变量 var a 所以现在传入的是 var a
b : undefined; //函数表达式也是申明的
//将其均赋值 undefined
}
在函数体里面找函数声明,值赋予函数体
AO{
a : function a () {}, //将函数体赋值给属性
b : undefined; //b是函数表达式,不是函数申明
}
预编译发生在执行函数之前
function f(a) {
console.log(a);
var a = 10;
console.log(a);
function a() {}
console.log(a);
var b = function () {}
console.log(b);
}
f(1);
第一句就是console.log(a)这时候我们上哪找这个a呢?
这个时候是去AO对象里面找的,所以输出的a是 函数a
第二、三句 var a = 10这一步其实是不完全执行,因为预编译的第二步已经找找到变量声明(变量 声明提升),将变量和形参名作为AO属性名,所以这一句执行的其实是 a = 10
这个时候,将AO对象的a的值修改为10,所以输出的是10
第四、五句 function a() {} 这句话在预编译第四步的时候提升上去了(函数声明整体提升) 所以就不看了,此时AO对象里面的a任然是 10 所以输出也是10
第六、七句虽然第六句和第二句类似,AO对象里b的值修改成是 函数b
所以也将输出函数b
最终输出如下
再来看一段代码练练手吧
function f(a,b) {
console.log(a);
console.log(b);
var b = 234;
console.log(b);
a = 123;
console.log(a);
function a(){}
var a ;
b = 345;
var b = function () {}
console.log(a);
console.log(b);
}
f(1);
结果
注意当函数退出的时候AO对象会被销毁
全局发生的预编译
- 生成了一个 GO 的对象 Global Object(window 就是 GO)
- 找形参和变量声明,将变量和形参名作为 GO 属性名,值为 undefined
- 在函数体里面找函数声明,值赋予函数体
预编译不止发生在函数体,还发生在全局
console.log(a);
var a = 1;
//输出undefined
console.log(a);
var a = 1;
function a() {}
//输出函数 a
全局的预编译和函数的预编译有点差别,它没有第三步,也就是说,他没有形参
同时它的第一步不是生成AO对象,是生成 GO对象(Global Object),其他的步骤都一样
GO === window, GO 和 window 是一个东西
任何全局变量都是 window 上的属性。没有声明就是赋值了,归 window 所有,就是在 GO 里面预编译
先生成 GO 还是 AO?
想执行全局,先生成 GO,在执行 test 的前一刻生成 AO
在几层嵌套关系,近的优先,从近的到远的,有 AO 就看 AO,AO 没有才看 GO
作用域精解
- [[scope]]:每个 javascript 函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供 javascript 引擎存取,[[scope]]就是其中一个。[[scope]]指的就是我们所说的作用域,其中存储了运行期上下文的集合。
- 作用域链:[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。
- 运行期上下文:当函数在执行的前一刻,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,执行上下文被销毁。
- 查找变量:在哪个函数里面查找变量,就从哪个函数作用域链的顶端依次向下查找。函数类对象,我们能访问 test.name
test.[[scope]]隐式属性——作用域
function test (){
}
// 第一次执行 test(); → AO{} // AO 是用完就不要的
// 第二次执行 test(); → AO{} // 这是另外的 AO
function a (){
function b (){
var bb = 234;
aa = 0;
}
var aa = 123;
b();
console.log(aa)
}
var glob = 100;
a(); // 0
0 是最顶端,1 是次顶端,查找顺序是从最顶端往下查
理解过程:bb 的 AO 是拿到 aa 的 AO,就是同一个 AO,bb 只是引用了 aa 的 AO,GO 也都是同一个。function b(){}执行完,干掉的是 b 自己的 AO(销毁执行期上下文)(去掉连接线),下次 function b 被执行时,产生的是新的 b 的 AO。b 执行完只会销毁自己的 AO,不会销毁 a 的 AO。function a(){}执行完,会把 a 自己的 AO 销毁【会把 function b 也销毁】,只剩 GO(回归到 a 被定义的时候),等下次 function a再次被执行时,会产生一个全新的 AO,里面有一个新的 b 函数。。。。。。周而复始
function a() {
function b() {
function c() {
}
}
b();
}
a();
/ * ------------------------------------------------------------------------------ */
a 被定义 ----------------------> a.[[scope]] → 0 : GO{}
a 被执行 ----------------------> a.[[scope]] → 0 : aAO{}
1 : GO{}
b 被定义 ----------------------> b.[[scope]] → 0 : aAO{}
1 : GO{}
b 被执行 ----------------------> b.[[scope]] → 0 : bAO{}
1 : aAO{}
2 : GO{}
c 被定义 ----------------------> c.[[scope]] → 0 : bAO{}
1 : aAO{}
2 : GO{}
c 被执行 ----------------------> c.[[scope]] → 0 : cAO{}
1 : bAO{}
2 : aAO{}
3 : GO{}
// 当 c 执行完后,会干掉自己的 cAO,回到 c 被定义的状态,当 c 再被执行时,会生
// 成一个新的 newcAO{},其余都一样,因为基础都是 c 的被定义状态
c 被执行 ----------------------> c.[[scope]] → 0 : newcAO{}
1 : bAO{}
2 : aAO{}
3 : GO{}
立即执行函数
前言
如果我在全局的范围内定义了一个函数
<script>
function a(){
}
function b(){
}
</script>
这两个函数除非等到执行之前,是不是一直处于等待被执行的情况,而一直等待被执行 是不是占用空间,除非等到JavaScript被执行完,才能把空间给释放掉
更特殊的时候,有的函数(例如针对初始化功能的函数)从出生倒塌执行完,他就被执行了一次,这种函数我们一般不想像上述那种定义方式来实现,我们给出了一种叫做 立即执行函数
立即执行函数(Imdiately Invoked Function Expression IIFE)
一个在定义的时候就立即执行的JavaScript函数
如下就是立即执行函数两种写法
(function (){
}())//W3C 推荐这种写法
(function (){
})();
我们用第一种写法进行解释
函数执行完就被销毁
为什么立即执行函数不用函数名
首先来看下下面这一段代码
(function abc() {
var a = 123;
var b = 234;
console.log(a + b);
}())
因为立即执行函数执行完就被销毁,所以,根本不需要添加函数名
立即执行函数除了执行完就被释放,除此之外,函数该有的东西,立即执行函数都有
立即执行函数的形参、实参以及返回值
(function (a,b,c) {
//第一个括号可以写形参
}(1,2,3))
//第二个括号里面可以传递实参
var num = (function () {
return 4;
}())
//通过外界变量传递返回值
深入浅出
如下函数申明
function test(){
console.log('a');
}
然后大家来看啊,你看,我们这个函数申明和函数执行是不是一模一样,那如果在函数申明后面加一对括号,是不是也能执行呢?
function test(){
console.log('a');
}() //结果是报错的,语法解析错误
function test(){
console.log('a');
}
test(); //这样才可以
为什么?别急,先记住一句话
只有表达式才能被执行符号执行
因为上述的第一种情况,在function后面加括号,那个不叫函数表达式,那个叫做函数申明
欸,那我们也知道有一种叫做函数表达式,那么可以被括号执行吗?
var test = function () {
console.log('a');
}();//控制台上输出了 a
+ function test(){
console.log('a');
}();//这样也是可以的 因为它是一个表达式了
(function test() {
console.log('a');
})();//这样也是可以的,因为加了括号也就变成了表达式了
结果告诉我们是可以的
再来一个知识点
能被执行符号执行的函数表达式,这个函数名字就会被自动忽略
上述同样的代码运行之后,在控制台上输入test
得到的结果如下
所以是不是,这个表达式定义出来之后,马上被执行,并且被释放了,所以可以说,能被执行符号执行的表达式,基本上就成了立即执行函数
(function (){}())
我们上面讲了,只有表达式才能被执行符号执行,那么我们这一条代码怎么理解呢?
其实是因为,最外层的括号先执行,所以内部的就是表达式了
所以最后的执行符号()就可执行了
闭包
当内部函数被保存到外部时,将会生成闭包。闭包会导致原有作用域链不释放,造成内存泄露。
内存泄漏就是内存占用,内存被占用的越多,内存就变得越来越少了,就像内存被泄露了一样
function a() {
function b() {
var bbb = 234;
console.log(aaa);
}
var aaa = 123;
return b;
}
var glob = 100;
var demo = a();
demo();
答案123。因为没有 b();此时 b 还是被定义的状态,和 a 执行的状态是一样的。function a(){}是在 return b 之后才执行完,才销毁。return b 让 a 执行时的 AO 被保存在外面。
return b 是把 b(包括 a 的 AO)保存到外部了(放在全局) 当 a 执行完砍掉自己的 AO 时,b 依然可以访问到 a 的 AO(因为 return b)
但凡是内部的函数被保存到外部,一定生成闭包
function a() {
var num = 123;
function b() {
num++;
console.log(num);
}
return b;
}
var demo = a();
demo();
demo();
// 答案 101,102
理解过程
a 被执行 0 : a AO: num = 100;
1 : GO: demo = a();
b 被执行 0 : bAO :
1 : aAO : num = 100;
2 : GO: demo = a();
在第一次执行 function b 时,num ++就把 aAO 变成{num : 101},当 function b 执行完毕时,剪断的是 bAO,而 aAO 不变,当执行 function a 的 return b 时就把 aAO,GO 都存在了外部,执行完 a 销毁 scope 时去掉 a 的连接线,但是因为 return b 把 aAO,GO存在了外部,所以依然还是可以访问值
在第二次执行 function b 时,aAO{num : 101},在 num ++就是 102
闭包的作用
一、实现公有变量
// 函数累加器
function add() {
var count = 0;
function demo() {
count++;
console.log(count);
}
return demo;
}
var counter = add();
counter();
counter();
counter();
counter();
counter();
每回调用 counter 就会在原有基础上加一次
二、可以做缓存(存储结构)
缓存是外部不可见的,但是确实有存储结构
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]();
myArr[1]();
答案101 和 100
思考过程:说明两个用的是一个 AO
test doing test[[scope]] 0:testAO
1:GO
a defined a.[[scope]] 0 : testAO
1 : GO
b defined b.[[scope]] 0 : testAO
1 : GO
return[a, b]将 a 和 b 同时被定义的状态被保存出来了
当执行 myArr[0]();时
a doing a.[[scope]] 0 : aAO
1 : testAO
2 : GO
当执行 myArr[1]();时
b doing b.[[scope]] 0 : bAO
1 : a 运行后的 testAO
2 : GO
a 运行后的 testAO,与 a doing 里面的 testAO 一模一样
a 和 b 连线的都是 test 环境,对应的一个闭包
function a 和 function b 是并列的,不过因为 function a 在前,所以先执行 num ++,在执行 num --
myArr[0]是数组第一位的意思,即 a,myArr[0]();就是执行函数 a 的意思;
myArr[1]是数组第二位的意思,即 b,myArr[1](); 就是执行函数 b 的意思
三、可以实现封装,属性私有化。
缓存的应用
对象里面可以用属性和方法
function eater() {
var food = '';
var obj = {
eat: function () {
console.log('i am eating ' + food);
food = '';
},
push: function (myFood) {
food = myFood;
}
}
return obj;
}
var eater1 = eater();
eater1.push('banana');
eater1.eat(); // i am eating banana
eat 和 push 操作的是同一个 food
在 function eater(){里面的 food}就相当于一个隐式存储的机构
obj 对象里面是可以有 function 方法的,也可以有属性,方法就是函数的表现形式
四、模块化开发,防止污染全局变量
闭包例子
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 < myArr.length; i++) {
myArr[i]();
}
输出为 10个10
原因是因为产生了闭包
在这个函数体中,当 arr[0] 时,console.log(i);的 i 是不变的,还是 i,等函数保存到外部之后,等执行的时候,才会去找 i 的值。这个赋值语句中,arr[0] = 函数;把一个函数体或者说是一个函数引用赋给数组的当前位,数组的当前位需要马上被索取出来的(数组现在是当前第几位,我们是知道的,因为这个是执行语句),当 for(var i = 0)时,arr[i]会变成 arr[0],但是这个 i 跟函数体里面的 console.log(i + '');里面的 i 是没有关系的,因为函数体 function(){}不是现在执行,不会在意函数里面写的是什么,不是现在执行那么里面的console.log(); 不会变成现实的值,不是现在执行就是函数引用(函数引用就是被折叠起来的,系统不知道里面写的是什么)在执行 myArr[j]();的时候,系统才会读 console.log(i);里面的语句在定义函数的时候是不看里面的,在执行的时候才看
我们让上面这个变成打印 0,1,2,3,4,5,6,7,8,9,用立即执行函数解决
function test() {
var arr = [];
for (var i = 0; i < 10; i++) {
(function (j) {
arr[i] = function () {
console.log(j + ' ');
}
}(i))
}
return arr;
}
var myArr = test();
for (var i = 0; i < myArr.length; i++) {
myArr[i]();
}
闭包的防范
闭包会导致多个执行函数共用一个公有变量,如果不是特殊需要,应尽量防止这种情况发生。
对象
var mrDeng = {
name: 'MrDeng',
age: 40,
sex: 'male',
health: 100,
somke: function () {
console.log('I am smoking');
this.health--;
},
drink: function () {
console.log('I am drinking');
this.health++;
}
}
改 mrDeng.health 为 this.health,此处 this 指代的是自己,是第一人称,指的就是 mrDeng。因为 this 是在一个方法里面,所以指的这个方法。
属性的增、删、改、查
对象的创建方法
(1) var obj = {} 对象字面量/对象直接量 plainObject
(2)构造函数
系统自带的构造函数 Object()
new Object();Array();Number();Boolean();Date();
系统自带的构造函数 Object()可以批量生成对象,每一个对象都一样,但是彼此相互独立。
在 Object()前面加个 new,变成 new Object()的执行,就会真正的返回一个对象,通过 return 返回,拿变量接受。var obj = new Object();
var obj = new Object();和 var obj = {};这样写区别不大
var obj = new Object();
obj.name = 'abc';
obj.sex = "male";
双引号和单引号都是表示的字符串,写双引号也可以写单引号,但是为了跟后端 php配合最好写单引号。如果要打印一个单个的引号,用正则表达式转义字符\
自定义
Object.create(原型)方法
例 function Person(){}
Person 是可以随便写的,也是构造函数
构造函数跟函数结构上没有任何区别
var person1 = new person();
必须用 new 这个操作符,才能构造出对象
构造函数必须要按照大驼峰式命名规则,但凡是构造函数就要大写,例如 TheFirNa
car1 和 car 是长得一样,但是是不同的两个 car。方法名和对象名尽量不一样
a 和 A 变量是两个变量,var car = new Car 里面 car 和 Car 是两个变量
构造函数内部原理
前提必须要加 new,以下三步都是隐式的:
- 在函数体最前面隐式的加上 var this = {} 空对象
- 执行 this.xxx = xxx;
- 隐式的返回 return this
包装类
new String(); new Boolean(); new Number();
var num =123; → 原始值数字
只有原始值数字是原始值,原始值不能有属性和方法
属性和方法只有对象有,包括对象自己,数组,function
var num = new number 123; → 构造函数。是对象 123,不是原始值数字
var num = new Nunber(123); //数字类型对象
var str = new String(‘abcd’); //字符串类型对象
var bol = new Boolean(‘true’); //布尔类型对象
undefined 和 null 不可以有属性
原型
- 定义:原型是 function 对象的一个属性,它定义了构造函数制造出的对象的公共祖先。通过该构造函数产生的对象,可以继承该原型的属性和方法。原型也是对象。
- 利用原型特点和概念,可以提取共有属性。
- 对象属性的增删和原型上属性增删改查。
- 对象如何查看原型 ==> 隐式属性 __proto__。
- 对象如何查看对象的构造函数 ==> constructor。
// Person.prototype -----原型
// Person.prototype = {}; -----祖先
Person.prototype.name = 'leo';
function Person() {
}
var person = new Person();
var person1 = new Person();
console.log(person.name);
person 和 person1 都有一个共有的祖先 Person.prototype
Person.prototype.LastName = 'leo';
Person.prototype.say = function () {
console.log('aaa');
}
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
var person = new Person('cy', 18, 'male');
自己身上有属性,原型上也有属性,取近的,用自己的
通过对象(后代)改原型(祖先)是不行的,在对象里面修改,只作用给自己改原型都不行,增加肯定也不行。对象可以删除属性
function Car() {
}
var car = new Car();
在原型内部自带 constructor,指的是 Car。通过 constructor 能找的谁构造的自己
constructor 可以被人工手动更改
浅粉色的__proto__是系统的隐式的属性,前面两个_后面两个_,可以修改,尽量不改。在开发的时候,如果很私人可以写成_private,告诉同事别动。
上面的__proto__放的是原型。__proto__存的对象的原型
上面的 var this ={__proto__:person.prototype};这个对象并不是空的,这个 proto,当你访问这个对象的属性时,如果对象没有这个属性,那么就会访问 proto 索引,看看有没有。有一个连接的关系,原型和自己连接到一起
Person.prototype.name 这种.的写法是在原有的基础上把值改了。改的是属性,也就是房间里面的东西。
而 Person.prototype={name:’cherry’}是把原型改了,换了新的对象。改了个房间。
上面在 new 的时候 var this = {__proto__:Person.prototype}里面的指向 Person,此时Person.prototype 与__proto__指向的是一个空间,把他返回给 var person。
先 new 再 Person.prototype={name:’cherry’}已经晚了
在 Person.prototype={name:’cherry’}时,Person.prototype 空间改了,但是__proto指向的空间不变。
上面的步骤实际上是→
就像
Person.prototype.name = 'sunny';
function Person() {
// var this = {_proto_: Person.prototype}
}
Person.prototype = {
name: 'cherry'
}
var person = new Person();
上面这种思考过程:程序执行顺序
- 先把 function Person(){}在预编译的过程中提到最上面
- 再执行 Person.prototype.name = 'sunny' 这一样行
- 再执行 Person.prototype = {name:'cherry'}
- 最后执行 var person = new Person();执行到 new 的时候,才会发生 //var this ={__proto__:Person.prototype}
- 下面的把上面的覆盖了
原型链
function Person(){}
这说明原型里面有原型
原型链
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.hobit = 'smoke';
}
var son = new Son();
执行 son.toString //返回 function toString( ){ [native code] },这里返回的是原型链终端的 toString
- 如何构成原型链?(见上一个例子)
- 原型链上属性的增删改查 : 原型链上的增删改查和原型基本上是一致的。只有本人有的权限,子孙是没有的。
- 谁调用的方法内部 this 就是谁-原型案例
- 绝大多数对象的最终都会继承自 Object.prototype
- Object.create(原型);
- 原型方法上的重写
son.fortune.card2=’master’这种改,这是引用值自己的修改。属于 fortune.name 给自己修改,这是一种调用方法的修改
Grand.prototype.lastName = 'Deng';
function Grand() {
}
var grand = new Grand();
Father.prototype = grand;
function Father() {
this.name = 'xuming';
this.fortune = {
card1: 'visa'
};
this.num = 100;
}
var father = new Father();
Son.prototype = father;
function Son() {
this.hobit = 'smoke';
}
var son = new Son();
son.num++是 son.num=son.num+1 是先把父级的取过来再赋值+1,所以爹的没变
Person.prototype = {
name: 'a',
sayName: function () {
console.log(this.name);
}
}
function Person() {
}
var person = new Person();
console.log(this.name); //如果写成 name 就会错,没有这个变量
Person.prototype = {
name: 'a',
sayName: function () {
console.log(this.name);
}
}
function Person() {
this.name = 'b';
}
var person = new Person();
//a.sayName ( )方法调用,就是 say.Name 里面的 this 指向,是谁调用的这个方法,this就指向谁
Person.prototype = {
height: 100
}
function Person() {
this.eat = function () {
this.height++;
}
}
var person = new Person();
var obj = { };也是有原型的
var obj = { };与 var obj1 = new Object( );效果是一样的
写 var obj = { }; 系统会在内部来一个 new Object( );
obj1.__proto__ → Object.ptototype;
但是在构造对象时,能用对象自变量 var obj = { };就不要用 var obj1 = new Object( );
对像自变量的原型就是 Object.ptototype;
Object.create(原型);
Object.create 也能创建对象。var obj = Object.create(这里必须要有原型)
var obj = {
name: 'sunny',
age: 123
}
var obj1 = Object.create(obj);
绝大多数对象的最终都会继承自 Object.prototype
Object.create()在括号里面只能放 null 或者 Object,其余会报错
而 new Number(num). toString 的原型是 Nunber.prototype,而 Nunber.prototype 上面有一个.toString 方法,Nunber.prototype 也有原型 Nunber.prototype.__proto__,原型是Object.prototype
假如 new Number 上面的 prototype 上面有这个 toString,那么就不用 Object.prototype的 toString。而这个 number 上面有这个 toString。
然后 number 上面的 toString 调用的是自己重写的 toString。原型上有这个方法,我自己又写了一个和原型上同一名字,但不同功能的方法,叫做重写(同一名字的函数,不同重写方式)通过返回值,形参列表不同传参同样的名实现不同功能的,就是重写
document.write 会隐式的调用 toString 方法,其实打印的是 toString 的结果
拓展
有个 bug,在控制台 0.14*100
出现 14.0000000000002,是 js 开发精度不准
向上取整 Math.ceil(123.234)
答案 124
向下取整 Math.floor(123.999)
答案 123
四舍五入取整round
Math.round(123.4) // 123
Math.round(123.5) // 124
Math.random()
产生一个 0 到 1 区间的开区间 随机数
call/apply
作用,改变 this 指向。
区别,后面传的参数形式不同。
function Person(name, age) {
this.name = name;
this.age = age;
}
var person = new Person('deng', 100);
var obj = {
}
Person.call();
任何一个方法都可以.call .call 才是一个方法执行的真实面目
直接执行 Person.call ( )和 Person ( )没有区别
function Person(name, age) {
this.name = name;
this.age = age;
}
var person = new Person('deng', 100);
var obj = {
}
Person.call(obj, 'cheng', 18);
如果 Person.call( obj );里面的 call 让 person 所有的 this 都变成 obj
不 new 的话,this 默认指向 window。call 的使用必须要 new
call 的第一位参数用于改变 this 指向,第二位实参(对应第一个形参)及以后的参数都当做正常的实参,传到形参里面去借用别人的方法,实现自己的功能。
call 改变 this 指向,借用别人的函数,实现自己的功能。只能在你的需求完全涵盖别人的时候才能使用。如果不想要 age 这个,就不能使用这种方法
Person.call(this, name, age, sex);里面的 this 现在是 new 了以后的 var this={}
利用 Person 方法,实现了 Student 自己的封装
function Wheel(wheelSize, style) {
this.style = style;
this.wheelSize = wheelSize;
}
function Sit(c, sitColor) {
this.c = c;
this.sitColor = sitColor;
}
function Model(height, width, len) {
this.height = height;
this.width = width;
this.len = len;
}
function Car(wheelSize, style, c, sitColor, height, width, len) {
Wheel.call(this, wheelSize, style);
Sit.call(this, c, sitColor);
Model.call(this, height, width, len);
}
var car = new Car(100, '花里胡哨的', '真皮', 'red', 1800, 1900, 4900);
apply 也是改变 this 指向的,只是传参列表不同,第一位也是改变 this 指向的人,第二位,apply 只能传一个实参,而且必须传数组 argunments call 需要把实参按照形参的个数传进去
new 以后才有意义
Wheel.apply(this, [wheelSize, style]);
Sit.apply(this, [c, sitColor]);
Model.apply(this, [height, width, len]);
继承
1.传统形式 ==> 原型链
// 问题:过多的继承了没用的属性
Grand.prototype.lastName = 'Ji';
function Grand() {
}
var grand = new Grand();
Father.prototype = grand;
function Father() {
this.name = 'hehe';
}
var father = new Father();
Son.prototype = father;
function Son() {
}
var son = new Son();
2.借用构造函数 ==>利用 call、apply
所以不算标准的继承模式
- 不能继承借用构造函数的原型
- 每次构造函数都要多走一个函数 ==>浪费效率
function Person(name, age, sex) {
this.name = name;
this.sex = sex;
this.age = age;
}
function Student(name, age, sex, grade) {
Person.call(this, name, age, sex);
this.grade = grade;
}
var person = new Student();
3.共享原型(较好的继承方法)
Father.prototype.lastName = 'Deng';
function Father() {
}
function Son() {
}
Son.prototype = Father.prototype;
var son = new Son();
var father = new Father();
可以用上面的方式封装函数,实现一个继承
extend 和 inherit 都是继承的意思。
inherit 是 css 的一个值,也是继承的意思。
文字类属性都有一个传递的特性:子元素没有设置文字类属性,子元素默认继承父
元素的属性。
在 inherit(Target,Origin)里面传进去的值是构造函数,需要大驼峰式书写,origin是原始的意思,让 target(目标)继承 origin
下面这种写法,son.prototype 和 father.prototype 指向的是一个房间,改 son 就改了father。我们希望 son 用的 father 的原型,但是改变 son 自己的属性不影响 father
Father.prototype.lastName = 'Deng';
function Father() {
}
function Son() {
}
function inherit(Target, Origin) {
Target.prototype = Origin.prototype;
}
inherit(Son, Father);
Son.prototype.sex = 'male';
var son = new Son();
var father = new Father();
4.圣杯模式
圣杯模式是在方法三的共有原型,但是在共有原型的基础上有改变。
共享原型是:son.prototype=father.prototype
圣杯模式是:另外加个构造函数 function F(){}当做中间层,然后让 F 和 father 共有一个原型 F.prototype=father.prototype,然后 son.prototype = new F();使用原型链形成了继承关系,现在改 son.prototype 就不会影响 father.prototype
function Father() {
}
function Son() {
}
Father.prototype.lastName = 'Deng';
function inherit(Target, Origin) {
function F() {};
F.prototype = Origin.prototype;
Target.prototype = new F();
}
inherit(Son, Father);
var son = new Son();
var father = new Father();
son._proto_ --> new F()._proto_ --> Father.prototype
原型上默认有个 constructor
constructor 默认指向他的构造函数
son. constructor 应该指向 Son
指向 father 就是混乱了
所以要指一下 ==>
我们希望我们构造出的对象,能找到自己的超类,超级父级(究竟继承自谁)应该起名为super 但这个是保留字,我们就以 uber
function Father() {
}
function Son() {
}
Father.prototype.lastName = 'Deng';
function inherit(Target, Origin) {
function F() {};
F.prototype = Origin.prototype;
Target.prototype = new F();
Target.prototype.constructor = Target;
Target.prototype.uber = Origin.prototype;
}
inherit(Son, Father);
var son = new Son();
var father = new Father();
建议使用的圣杯模式
var inherit = (function () {
var F = function () {};
return function (Target, Origin) {
F.prototype = Origin.prototype;
Target.prototype = new F();
Target.prototype.constructor = Target;
Target.prototype.uber = Origin.prototype;
}
}());
命名空间(其实就是对象)
管理变量,防止污染全局,适用于模块化开发
多人开发,对象命名容易重复,就要解决命名空间的问题
右边是命名空间老旧的解决方 ==>
下面是现在公司最常见的方法:用闭包来解决(也可用 webpack),返回方法的调用。
init 是初始化,入口函数,入口名字。init 调用了这个函数的功能
var name = 'bcd';
var init = (function () {
var name = 'abc';
function callName() {
console.log(name);
}
return function () {
callName();
}
}())
var initDeng = (function () {
var name = 123;
function callName() {
console.log(name);
}
return function () {
callName();
}
}())
对象拓展
属性的表示方法(查看属性)
obj.prop 查看就用.XXXX obj[“prop”] 中括号也是访问属性的方法
var obj = { name : 'abc' }
用方括号来访问属性也是一样的(里面必须是字符串)
这两种基本上完全相同 obj.name → obj [ 'name' ]
想实现属性名的拼接,只能用方括号的形式
var deng = {
wife1: {
name: 'xiaoliu'
},
wife2: {
name: 'xiaozhang'
},
wife3: {
name: 'xiaomeng'
},
wife3: {
name: 'xiaowang'
},
sayWife: function (num) {
return this['wife' + num];
}
}
对象的枚举 enumeration
for in 循环(简化版 for 循环),目的是便利对象,通过对象属性的个数来控制循环圈数,这个对象有多少属性循环多少圈,而且在每一圈时,都把对象的属性名放到 Prop里面 在枚举里面,一定要写成 obj[prop]不能加字符串
var obj = {
name: '123',
age: 123,
sex: 'male',
height: 180,
weight: 75
}
for (var prop in obj) {
console.log(prop + ' ' + typeof (prop));
}
上面就是 for in 循环,就是遍历用的。通过对象的属性个数来控制循环圈数,有多少个属性就会循环多少圈。
for(var prop in obj)在循环每一圈的时候,他会把对象的属性名放在 prop 里面。想遍历谁就 in 谁,prop 可以写别的,obj 就是我们想要遍历的对象。
console.log(obj.prop)
循环内若写obj.prop 系统以后我们写的是 obj[‘prop’],系统会以为我们是在让他访问 prop这个属性,不会把 prop 当成一个变量来使用。写成 obj[prop]就可以成功访问了。
var obj = {
name: '123',
age: 123,
sex: 'male',
height: 180,
weight: 75
}
for (var prop in obj) {
console.log(obj[prop]);
}
上述写法会把原型上面的东西也拿出来
Person.prototype.lastName = 'LastName';
function Person() {
this.name = '123';
this.age = 123;
this.sex = 'male';
}
var person = new Person();
for (var prop in person) {
console.log(person[prop]);
}
如果在遍历的时候,我们不想把原型上面的属性拿出来,可以用 hasOwnProperty,一般与 for in 循环成套出现
hasOwnProperty 是一个方法,来判断这个对象是你自己的还是原型的, 任何一个对象里面都有 hasOwnProperty,里面是需要传参的,把属性名传进去(如 prop)。
下面达到了如果不是自己的属性,是原型上的属性,就不会返回。
Person.prototype.lastName = 'LastName';
function Person() {
this.name = '123';
this.age = 123;
this.sex = 'male';
}
var person = new Person();
for (var prop in person) {
if (person.hasOwnProperty(prop)) {
console.log(person[prop]);
}
}
for in 循环理论上可以返回原型和原型链上的东西,一旦这个原型链延展到了
的 object.prototype 上,不会打印系统的,只会打印自带的。
in 操作符:很少用
in 操作符你的也是你的,你父亲的也是你的,只能判断这个对象能不能访问到这个属性,包括原型上;不是判断属性属不属于这个对象的
instanceof 操作用法类似于 in,但是完全不同
A instanceof B 的意思是 A 对象是不是 B 构造函数构造出来的;记住是:看 A 对象的原型链上有没有 B 的原型
function Person() {
}
var person = new Person();
拓展,区别对象和数组
var arr = [];
var obj = {};
三种方法
- instanceof
- constructor
- Object.prototype.toString.call()
this
如何判断this指向?可以按照下面的顺序来进行判断:
// 1. 函数是否在new中调用(new绑定)? 如果是的话,this,绑定的就是新创建的对象
var bar = new foo();
// 2. 函数是否通过call、apply(显示绑定)或者硬邦定(bind)调用?如果是的话,this绑定的是指定对象
var bar = foo().call(obj2);
// 3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的就是上下文对象
var bar = obj.foo();
// 4. 如果都不是的话,则使用默认绑定,如果在严格模式下,就绑定到undefined,否则则绑定到全局对象
var bar = foo();
1.默认绑定。在严格模式下绑定到undefined,否则绑定到全局对象
解释:
最常用的函数调用类型是:独立函数调用。可以把这条规则看做是无法应用其他规则时的默认规则。如:
function foo () {
console.log(this.a)
}
var a = 2;
foo(); // 2
在本例中,foo函数调用时应用了默认绑定,因此this指向全局对象,而声明在全局作用域中的变量(比如 var a = 2)就是全局对象的一个同名属性,所以在调用foo()时,this.a被解析成了全局变量a。
那么,我们是怎么知道这里应用了默认绑定呢?
在代码中,foo()函数时直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。
如果使用严格模式(strict mode),则不能将全局对象用于默认绑定,因此this会绑定到undefined:
function foo() {
"use strict";
console.log(this.a);
}
var a = 2;
foo(); // 会报错。TypeRrror: this is undefined。
这里还有一个非常重要的细节,虽然this的绑定规则完全取决于调用位置,但是只有foo()运行在非strict mode下时,默认绑定才能绑定到全局,在严格模式下调用foo()则不影响默认绑定:
function foo() {
console.log(this.a);
}
var a = 2;
(function () {
"use strict";
foo(); // 2
})()
一般来讲,不应该在代码中混合使用strict模式和非strict模式。整个程序要么严格要么非严格。然而,有时候可能会用到第三方库,其严格程度和自己的代码有所不同,因此一定要注意这类兼容性细节。
2. 隐式绑定。由上下文对象调用,绑定到那个上下文对象
解释:
确定调用位置是否有上下文对象,或者说是否被某个对象拥有或包含,如:
function foo () {
console.log(this.a)
}
var obj = {
a: 2,
foo: foo
}
obj.foo(); // 2
调用位置会使用obj上下文来引用函数,因此,可以说函数被调用时obj对象“拥有”或“包含”它。
当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象。因此this.a
和obj.a
是一样的
对象属性引用链中只有上一层或者说最后一层在调用位置中起作用,如:
function foo() {
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo,
}
var obj1 = {
a: 2,
obj2: obj2,
}
obj1.obj2.foo(); // 42
隐式丢失
被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定绑定,从而把this绑定到全局对象或者undefined上。如下:
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo; // 函数别名
var a = 'global'; // a 是全局对象的额属性
bar(); // 'global'
虽然bar是obj.foo的一个引用,但是实际上,它引用的是foo函数本身,因此此时的bar()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
在传入回调函数时,也会发生隐式丢失:
function foo () {
console.log(this.a);
}
function doFoo (fn) {
// fn 其实引用的是foo
fn();
}
var obj = {
a: 2,
foo: foo
}
var a = 'global';
doFoo( obj.foo ); // 'global'
和上一个例子一样,fn引用的是foo函数本身,因此此时的fn()其实是一个不带任何修饰的函数调用,因此应用了默认绑定。
在js环境中内置的setTimeout()函数实现和下面的伪代码类似:
function setTimeout(fn, delay) {
// 等待delay毫秒
fn();
}
所以传递给定时器的函数,也会应用默认绑定。
3. 显示绑定。由call/apply/bind显示绑定,绑定到执行的对象
解释:
可以使用call
、apply
方式绑定对象,如:
function foo () {
console.log(this.a);
}
var obj = {
a: 2
}
foo.call(obj); // 2
foo.apply(obj); // 2
通过foo.call(...),我们可以在调用foo时强制把它的this绑定到obj上。
硬绑定
通过code
、apply
仍然无法解决问题,但是通过显示绑定的一个变种可以解决问题,如:
function foo() {
console.log(this.a);
}
var obj = {
a: 2
}
var bar = function () {
foo.call(obj);
}
bar(); // 2
setTimeout(bar, 100); // 2
bar.call( window ); // 2。强制绑定的bar不可能再修改它的this。
为什么会出现这样的情况呢?
是由于我们创建了函数bar()
,并在它的内部手动调用了foo.call(obj)
,因此强制把foo
的this
绑定到了obj
。无论之后如何调用函数bar
,它总会手动在obj
上调用foo
。这种绑定是一种显式的强制绑定,因此我们称之为硬绑定。
由于硬绑定是一种非常常用的模式,所以ES5提供了方法bind,使用方法:
function foo(something) {
console.log(this.a, something);
return this.a + something;
}
var obj = {
a: 2,
}
var bar = foo.bind(obj);
var b = bar(3);// 2 3
console.log(b); // 5
bind 会返回一个新函数,它会把指定的参数设置为this的上下文并调用原始函数。
4. new绑定。由new调用,绑定到新创建的对象
解释:
使用new来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
- 创建(构造)一个全新的对象。
- 这个新对象会被执行[[Prototype]]连接。
- 这个新对象会绑定到函数调用的this。
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function foo(a) {
this.a = a;
}
var bar = new foo(2);
console.log(bar.a); // 2
使用new来调用foo(...)时,我们会构造一个新对象并把它绑定到foo(...)调用中的this上。
注意:ES6中的箭头函数不会使用四条标准的绑定规则,而是根据当前的词法作用域来决定this,具体来说,箭头函数会继承外层函数调用的this绑定(无论this绑定到什么)。这其实和ES6之前的代码中的self=this机制一样。
this指向这部分内容借鉴于
arguments
arguments.callee 指向函数的引用(函数自己)
function test() {
console.log(arguments.callee);
}
例我们要初始化数据,是 100 以内的阶乘,用立即执行函数
找到自己的引用来解决
var num = (function (n) {
if (n == 1) {
return 1;
}
return n * arguments.callee(n - 1);
}(100))
function test() {
console.log(arguments.callee);
function demo() {
console.log(arguments.callee);
}
demo();
}
在哪个函数里面的 arguments.callee 就指代了哪个函数
caller 谁调用他,caller 不能用在 arguments 里面
function test() {
demo();
}
function demo() {
console.log(demo.caller);
}
test();
demo 被调用的环境是 test
所以这个 caller 指代的 test
克隆
浅层克隆
要将 对象 obj 克隆给对象 obj1
//浅层克隆实现
var obj = {
name: 'abc',
age: 20,
sex: 'male',
arr: ['aaa', 'bbb', 'ccc']
}
function copy(origin, target) {
var target = target || {};
//为了防止用户不传 target(容错),给了参数就直接用,不给就当空对象
for (var prop in origin) {
target[prop] = origin[prop];
}
return target;
}
var obj1 = copy(obj);
然而,浅层克隆存在相应的问题
浅层克隆为什么会出现这种问题
首先我们需要清楚 JavaScript 中的数据类型总体来说分为两种,他们分别是:基本数据类型 和 复杂数据类型
基本数据类型(值类型):数值型(Number),字符类型(String),布尔值型(Boolean),null 和 underfined
复杂数据类型(引用数据类型):函数,对象,数组等
- 基本数据类型:这些值都有“固定的大小”,往往都保存在"栈内存"中(闭包除外)。变量之间的互相赋值,是指开辟一块新的内存空间,将变量值赋给新变量保存到新开辟的内存里面;之后两个变量的值变动互不影响
- 复杂数据类型:引用数据类型的值是保存在堆内存中的对象。变量之间的互相赋值,只是指针的交换,而并非将对象(普通对象,函数对象,数组对象)复制一份给新的变量,对象依然还是只有一个,只是多了一个指引
因此在浅层克隆中
- 基本数据为值传递,复杂数据类型(对象,数组)仍为引用传递。引用传递使对象指向相同的地址空间,改变一个对象,另一个也会随之改变。
而我们需要的克隆 是所有元素或属性均完全复制,与原对象完全脱离,也就是说所有对于新对象的修改都不会反映到原对象中。
深度克隆思路
需要一个分析环节,分析待拷贝的是什么,是原始值就按原来的方法拷贝过来,是引用值就分析是数组还是对象。如果是数组,就新建一个数组;如果是对象,就新建一个对象。再一层层看,形成一个递归。
思路步骤
- 先把所有的值都遍历一遍(看是引用值和原始值)
- 判断是原始值,还是引用值?用 typeof 判断是不是 object。
-
- 如果是原始值就直接拷贝。
- 如果是引用值,判断是数组还是对象。
- 判断是数组还是对象
-
- 如果是数组,就新建一个空数组。
- 如果是对象,就新建一个空对象。
- 建立了数组以后,如果是挨个看原始对象里面是什么,都是原始值就可以直接copy过来了;或者,建立了对象以后,挨个判断对象里面的每一个值,看是原始值还是引用值
- 递归
知识点补充——如何区分 数组 和 对象
1.instanceof
可通过 instanceof Array 来判断是否是数组
2.constructor
同样,也可通过construct 判断其构造函数来区分 数组 和 对象
3.toString
这里,我们使用
Object.prototype.toString.call(a)
来实现区分(不是用.toString() )
深度克隆代码实现
var obj = {
name: 'abc',
age: 20,
sex: 'male',
arr: ['aaa', 'bbb', 'ccc', {
qq: 12345,
email: '12345@qq.com'
}]
}
function deepCopy(origin, target) {
var target = target || {};
// 为了防止用户不传 target(容错),给了参数就直接用,不给就当空对象
toStr = Object.prototype.toString;
// 给函数创建一个引用 简化代码
var arrStr = '[object Array]';
// 给该比对字符串创建一个引用 简化代码
// var objStr = '[object Object]';
for (var prop in origin) {
// 先判断是不是原型上的属性,如果是false 就是原型上的属性,而我们不需要拷贝原型上的属性
if (origin.hasOwnProperty(prop)) {
if (typeof (origin[prop]) !== 'null' && typeof (origin[prop]) == 'object') {
// 判断是 引用值 再区分数组和对象
if (toStr.call(origin[prop]) == arrStr) {
// 是数组 创建空数组
target[prop] = [];
} else {
// 是对象 创建空对象
target[prop] = {};
}
deepCopy(origin[prop], target[prop]);
} else {
// 判断是 原始值 直接进行赋值
target[prop] = origin[prop];
}
}
}
return target;
}
var obj1 = deepCopy(obj);
更改前
更改后的obj的arr属性并未被更改
数组
一、数组的定义(来源于 Array.prototype)
1)new Array(length/content); var arr = new Array(1,2,3,4,5 );
2)字面量 var arr = [1,2,3,4,5];
二、数组的读和写
可以写 var arr = [,] //稀松数组,相当于定了两个位置,conlogo 结果是 empty
var arr = [ ];和 var arr = new Array( );唯一的区别是在 var arr = new Array( );只传了一个参数的情况下,会被当成长度,并且成为一个稀松数组
数组常用的方法
一、改变原数组(在原来数组基础上去改变)
1)reverse,sort,push,pop, unshift, shift,
2)splice
1. push
push 是在数组的最后一位添加数据,可以添加一个,也可以添加很多个,并且返回数组的长度
2.pop
pop 是剪切方法(把最后一位数剪切出去)。在 pop( )括号里面不能传参,写了会忽略
3.unshift
unshift 是从第一位加东西,并且返回添加之后的数组长度
4.shift
shift 是从第一位开始减东西,并且剪切出数组的第一位
5.reverse
数组逆反操作
6.splice
arr.splice(从第几位开始,截取多少长度,传参在切口处添加新的数据)
// 这里的-1 是倒数第一位
sort
给数组排序(按照从小到大),改变原数组
下面这个是按 asc 码排序的
sort 按 asc 码排序的
1 必须写两形参
2 看返回值 return
1)当返回值为负数时,那么前面的数放在前面,
2)当返回值为正数时,那么后面的数在前,
3)为 0,不动
var arr = [1, 3, 5, 4, 10];
arr.sort(function (a, b) {
if (a > b) {
return 1;
} else {
return -1;
}
})
// 实现了升序排序
将 if 条件内更改为 a < b 实现降序排序
var arr = [1, 3, 5, 4, 10];
arr.sort(function (a, b) {
return a - b; // 升序
})
b - a 降序
直接调用 arr.sort( )比的是 asc 码,要在里面填函数才可以
数组乱序
arr.sort(function (a, b) {
return Math.random() - 0.5; // 数组乱序
})
二、不改变原数组
1)forEach filter map reduce reduceRight
2)slice concat,join—>split,toString
concat
concat 连接, 把后面的数组拼到前面,并成立一个新的数组,不影响之前的两个数组。不能改变原数组
toString
toString 是把数组当做字符串展示出来
slice
slice 从该位开始截取,截取到该位,并不改变原数组,这里也可以写负数
// slice(从该位开始,截取到该位)
slice 里面可以填 0 个参数,也可以填 1 个参数,也可以填两个参数
1、如果填两个参数,slice(从该位开始截取,截取到该位)
如 arr.slice(1,2)从第一位开始截取,截取到第二位
2、如果填一个参数,从第几位开始截取,一直截取到最后。
如果 arr.slice(1) ,从第 1 位开始截取,截取到最后一位
3、不写参数就是整个截取数组(把类数组转换成数组的时候经常使用到)
join
join 括号里面需要用字符串形式(标准语法规定),就会用加的东西连接起数组
类数组
1、可以利用属性名模拟数组的特性
2、可以动态的增长 length 属性
3、如果强行让类数组调用 push 方法,则会根据 length 属性值的位置进行属性的扩充。
function test() {
console.log(arguments);
arguments.push(7);
}
test(1, 2, 3, 4, 5, 6);
这个看着像数组,但是数组有的方法,他全部都没有,所以他是类数组
类数组长得像数组,但是没有数组所拥有的的方法。
下面是类数组的基本形态
var obj = {
"0": 'a',
"1": 'b',
"2": 'c',
"length": 3,
"push": Array.prototype.push
}
类数组:属性要为索引(数字)属性,必须要有 length 属性,最好加上 push 方法。
如果给一个对象加上 splice 方法,那么这个对象就长得像数组了。但他仍然是对象,但是可以当做数组来用,需要自己添方法。(如下)
var obj = {
"0": 'a',
"1": 'b',
"2": 'c',
"length": 3,
"push": Array.prototype.push,
"splice": Array.prototype.splice
}
阿里巴巴题目,问这个 obj 长什么样子?
var obj = {
"2": 'a',
"3": 'b',
"length": 2,
"push": Array.prototype.push
}
obj.push('c');
obj.push('d');
其实,Array 的push是如下
Array.prototype.push = function (target) {
this[obj.length] = target;
obj.length++;
}
如果对象 obj 调用这个方法,那么 this 变成了 obj
上面的题目解释:
关键点在 length 上面,根据 length 改变而改变,走一下 length
var obj = {
"1": 'a',
"2": 'b',
"3": 'c',
"length": 3,
"push": Array.prototype.push
}
obj.push('d');
try...catch
try 花括号{里面会正常执行,但是遇到 b 报错时 b 就执行不出来,后面的代码 c 就不执行了,但是外面的代码 d 还能执行}catch(e),这个 e 可以随便写,写 abc 都可以,也是个形参
try {
console.log('a');
console.log(b);
console.log('c');
} catch (error) {
}
console.log('d'); // 输出 a d 不发生报错
// try内遇到错误不执行后序代码,但是 try外的仍然可以执行
如果 try 里面的代码不出错,在 catch 里面的代码就不执行;
如果 try 里面的代码出错,catch 负责补抓到错误信息封装到里面(error.massage error.name),错误对象只有 message 和 name。
try {
console.log('a');
console.log(b);
console.log('c');
} catch (e) {
console.log(e.message + ' ' + e.name);
}
console.log('d');
Error.name 的六种值对应的信息:
(前面是错误名称,后面是错误信息)
1. EvalError:eval()的使用与定义不一致
//eval 是不被允许使用的
2. RangeError:数值越界
3. ReferenceError:非法或不能识别的引用数值
//未经声明就使用,没有定义就使用
4. SyntaxError:发生语法解析错误
// Syntax 是语法解析()
5. TypeError:操作数类型错误
6. URIError:URI 处理函数使用不当
//引用地址错误
大部分都是 3 和 4 这种错误
伪代码也可以写了,可以写 var 老邓 = 123;这就是伪代码
var str = avs ==> ReferenceError
es5严格模式
es5.0 严格模式
(这章就是讲 es3.0 和 es5.0 产生冲突的部分)
浏览器是基于 es3.0 和 es5.0 的新增方法使用的。
如果两种发生了冲突,就用 es3.0。
es5.0 严格模式是指 es3.0 和 es5.0 产生冲突发部分就是用 es5.0,否则就用 es3.0。
es5.0 严格模式的启动“use strict” ;
用法在整个页面的最顶端写“use strict”,可以写在全局的最顶端,也可以写在某函数(局部)的最顶端,推荐使用局部的。
use strict” ;
不再兼容 es3 的一些不规则语法。使用全新的 es5 规范。两种用法:
1、全局严格模式
2、局部函数内严格模式(推荐)
就是一行字符串,不会对不兼容严格模式的浏览器产生影响。
不支持 with,arguments.callee,function.caller,变量赋值前必须声明,局部 this必须被赋值(Person.call(null/undefined) 赋值什么就是什么),拒绝重复属性和参数