ECMAScript,DOM,BOM
javascript分为三部分:
ECMAScript,DOM,BOM
Brendan Eich完全创建了ECMAScript;
DOM,BOM也可以算他创建的,不过他只是给出了规则
- DOM(document object model) 通过js操控HTML;
- BOM(browser object model) 通过js操控浏览器;
- (注意:CSS是不能被操作的)
Brendan Eich给出了DOM,BOM的标准,由各个厂商补充方法,所以DOM,BOM是各个厂商结合相同的ECMAScript,结合出的产物
因此,每个浏览器在DOM,BOM上可能有细微的差别
es5严格模式
JavaScript一直在发展,
通用的版本是:es3.0,es5.0,es6.0
且其中es3.0,es5.0特别通用
补充:一个版本的升级和革新,意味着:
- 会摈弃一些老语法(可能新版本的有些语法和老版本的语法产生了冲突,因此产生冲突的东西要适当的摈弃掉)
- 还产生一些新的语法(原来一些老的语法里面没有的)
- 注意:在es3.0,es5.0中可能有些语法重复了,但是在不同的版本中体现的方法和得出的结果不一样。
es5严格模式的源由:
现在的浏览器是基于es3.0和es5.0的那些新增的方法使用的。然而,在es3.0和es5.0必然会有冲突的部分,这些冲突的部分,浏览器就是使用es3.0的(当启用es5严格模式后,这些冲突的部分就使用es5.0的)
es5严格模式的启用:
es5.0要启动,要遵循一个语法规则(严格模式):在逻辑最前面写"use strict";
这是规定
"use strict";//可以写在全局-逻辑最顶端
//也可以写在局部-逻辑最顶端(局部函数中逻辑最前面,推荐)
//为什么这个语法规则要写成字符串形式?
//不会对不兼容严格模式的浏览器产生影响。
es5严格模式报错:
在es3中可以使用的方法,但是es5中不能使用
- 不能使用arguments.callee和function.caller
- 变量赋值前必须声明
- 局部的this必须赋值(传啥就是啥),预编译过程this不再默认指向window(注意:在全局中this依旧默认指向window)
- 拒绝重复属性或重复参数
- 注意: eval(“”) 在es3.0不能使用,通用的规定
- 使用with(){}会报错(with会把传进来的对象参数,变成with代码段的作用域链的最顶端)
//with还可以用来简化代码
with(document) {
write('a'); //document中有很多的方法
}
//但是with太强大了,可以改变作用域链,系统内核会消耗大量的效率,程序会变得很慢
//所以es5.0中,把with方法给禁止了
es5严格模式,缩减了代码的灵活度,但减少了你的犯错误的几率
注意:以下知识点都是es3.0包含es5.0的东西,这没什么好区分的
变量
- 变量名必须以 英文字母、_、$ 开头
- 变量名可以包括英文字母、_、$、数字
- 不可以用系统的关键字、保留字作为变量名
- 区分大小写
- 类型不需要定义,由值决定类型
数据类型概要:
javascript是动态语言
解释一行,编译一行,所以变量由值确定类型
并且,解释性语言(弱数据语言)永远不可能输出地址
值类型有:原始值,引用值(他们的赋值有区别)
原始值:
- Number(=右边为数字,数字是浮点型)
- String(=右边的内容用单引号或双引号包含起来)
- Boolean(=右边的值有true或false)
- undefined(=右边的值只有undefined,意为未定义)
- null(=右边值为null,意为空,一般用作覆盖某些变量的值)
引用值:
- function(函数)
- array(数组)
- object(对像)
原始值和引用值的区别:
例一:
var a = 10;
var b = a;
a = 20;
(a的值改为20,但b的值仍旧是10)
原始值存在stack(栈)里面
栈的一个规则:先进后出
栈内存和栈内存之间的赋值是copy,互不影响
例二:
var arr = [1];
var arr1 = arr;
arr.push(2);
(arr和arr1都改为1,2)
引用值存在heap(堆)里面
堆heap内存是引用值(值放入堆heap内存中,值对应的地址放进栈stack中)
例三:
var arr = [1,2];
var arr1 = arr;
arr = [1,3];
(arr的值改变,但是arr1的值不变,因为 arr = [1,3]; 是arr重新在堆heap里申请了新的空间,且值对应的存在栈stack里的地址得到改变,而arr1的堆heap的值和栈stack的地址都没变)
typeof
typeof()用来返回括号中的值的类型
六种数据类型:
- number(infinity,NaN属于number类型)
- string
- boolean
- undefined
- object(注意:null,数组,对象的数据类型是object)
- function
typeof调用方法:
- typeof(变量)
- typeof 变量
一般,如果访问没定义的变量,会报错
但有一极特殊的情况,
当且仅当:未定义的变量放入typeof()中不会报错
result: undefined(是字符串类型的)
类型转转
考虑原始值的类型转换即可,引用值的类型转换没意义
显示类型转换
Number() :
例一:
var num = Number('123');
//等价于 var num = 123;
例二:
一般明显是非数的转换为Number,为NaN
var demo = undefined;
var num = Number(demo);
console.log(typeof(num) + ":" + num);
result: Number : NaN
例三:
var demo = "123abc";
var num = Number(demo);
console.log(typeof(num) + ":" + num);
result: Number : NaN
例四:
var demo = "-123";
var num = Number(demo);
console.log(typeof(num) + ":" + num);
result: Number : -123
例五:
Number(true)的结果是 1
Number(null)的结果是 0
parseInt() :
一:括号内的数据类型转换为整型,将字符串和数字转换为整型,而其余的为NaN
二:parseInt(),括号内可以写两个参数,右边的是基底radix
三:parseInt(),括号内的数据,从第一位数字位看起,看到非数字位停止(若第一位不是数字,则返回NaN)
例一:
var demo = "123";
var num = parseInt(demo);
console.log(typeof(num) + ":" + num);
result: Number : 123
例二:
var demo = true;
var num = parseInt(demo);
console.log(typeof(num) + ":" + num);
result: Number : NaN
例三:
var demo = "123.9";
var num = parseInt(demo);
console.log(typeof(num) + ":" + num);
result: Number : 123
二:parseInt(),括号内可以写两个参数,右边的是基底radix
例一:
var demo = "10";
var num = parseInt(demo,16);
console.log(typeof(num) + ":" + num);
result: Number : 16
基底是16,所以左边参数认为为16进制的,然后转换为十进制
例二:
注意:radix的范围一般2-36,代表的是进制,对应的不合理的数结果为NaN
var demo = "3";
var num = parseInt(demo,2);
console.log(typeof(num) + ":" + num);
result: Number : NaN(例二中radix为2,二进制数不存在3)
parseFloat() :
与parseInt()有许多相似之处
区别:parseFloat是转换为浮点型,而且只能有一个参数
String() :
无论是什么,都转换为字符串
toString() :
- 写法有一些不同
var demo = 123; var num = demo.toString();
- 也可以转换为字符串(注意,undefined和null不能toString,不然会报错)
- toString()括号中也可以有参数,toString(radix),不过这个radix是目标进制(和parseInt()中的第二个参数是基底不同),若radix为8,即将十进制的数转换为八进制的数
Boolean() :
转换为布尔值
只有六种值对应的boolean值是false;
- undefined
- null
- NaN
- “” (空字符串“”,和空格字符串” “不一样)
- false
- 0
隐式类型转换
一元正负,加减乘除取余;大于小于等于……(有数字);
都先隐式的调用Number()
与或非,先隐式的调用Boolean()
isNaN() :
自己写一个isNaN()的功能:
function isNaN(num) {
var ret = Number(num);
ret += "";
if (ret == "NaN") {
return true;
}
else return false;
}
isNaN()会先隐式的调用Number(),再判断是不是NaN,是则为true,否则为false
几个特殊知识点(undefined,null,NaN):
- undefined和null 与数字0比较的结果都是false(无论是大于小于,还是等于)
- undefined == null;的结果是true
- NaN == NaN; 的结果是false(NaN不等于任何数,包括它自己)
避免发生隐式类型转换 :
绝对等于 ===
绝对不等与 !==
基本语法
- 语句后面要用分号结束“;”(英文字符的分号)
- 书写格式要规范,运算符两边都应该有空格
编译错误
一个html文件可以引入多个js代码块,不同代码块的错误相互不影响,但是不同代码块的变量可以相互使用,在运行之前会通篇检查是否有低级错误,若没有,则从上至下依句执行。
- 低级错误(语法解析错误)——该代码块一行都不会执行
- 逻辑错误(标准错误,情有可原)——该代码块中,执行到错误处终止执行
try{}catch(e){}
前提准备(Error.name的六种植对应的信息):
- EvalError:eval()的使用与定义不一致,eval()是不准我们用的,用了会报错,(很少见)
- RangeError:数组越界,(少见)
- ReferenceError:非法或不能识别的引用数值,(常见)
- SyntaxError:发生语法解析错误,(常见)
- TypeError:操作数类型错误
- URIError:URI处理函数使用不当 ,(多见于引用地址)
try {
}catch (e) {
}
作用:防止我们报错的
假如你写的代码中有一百行代码不确定会不会出错,但是无论其出不出错,都不希望这一百行代码中的出错代码影响到后续代码的执行
有些时候错误不是你能把控的,比如:假如设置一个变量data,赋值为null,当后端给data传递数据进来,传完后,我们那这个数据来用,但是你不知道什么时候传,传的成不成功,这个时候网速通不通畅
编程,是前端,后端拼到一起,后端的问题影响前端,前端的问题影响后端,都是有可能的
把代码写到 try 里面,代码会正常执行,但是它有一个特点:
try{
console.log("a");
console.log(b);
console.log("c");
}catch(e){
console.log(e.name + ":" + e.message);
}
console.log("d");
result:
a
ReferenceError : b is not defined
d
执行到 console.log(b); 这一句,按正常逻辑,会报错且后面的代码不会执行(看都不看),但是 try 里面的代码逻辑是:报错的代码依然报错,但不抛出错误( console.log(b); 这一行代码报错后,try里面的代码停止执行,但是 try 外面的代码依然继续执行)
catch 的作用是:当 try 里面的代码出错,就会执行 catch 里面的代码(实质上是捕捉 try 里面代码的错误信息)
错误有一堆错误信息,系统会把这些错误信息(error.name,error.message)封装到一个对象里面(error对象),然后传到形参e里面
运算符
+,/,比较运算符,&&,|| 的知识点较为值得书写
+
任何数据类型加字符串都等于字符串
var a = 1 + 1 + "a" + 1 + 1;
result: 2a11 (字符串类型)
/
例一:
var num = 1 / 0;
result: infinity(属于number类型)
例二:
var num = -1 / 0;
result: -infinity(属于number类型)
例三:
var num = 0 / 0;
result: NaN(Not a Number,也属于number类型)
比较运算符
例一:
var a = "a" > "b";
result: false(字符串比较的是ASCII码)
例二:
var a = 1 == 2;
result: false
例三;
var a = NaN == NaN;
result: false(这个是特例,因为NaN不等于任何数,包括它自己)
例四:
注意:引用值和引用值之间的比较,比较的是地址
var a = {} == {};
result: false
例五:
var obj = {};
var obj1 = obj;
var a = obj1 == obj;
result: true
逻辑运算符
只有六种值对应的boolean值是false;
- undefined
- null
- NaN
- “” (空字符串“”,和空格字符串” “不一样)
- false
- 0
&&
例一:
var a = 1 && 2 + 2;
result: 4
从左到右依次判断真假,是真就往后走,遇到假就返回该值
注意: &&有中断作用
例二:
2 < 1 && document.write("zgh666");
前面的为假,后面的语句不执行
例三:
2 > 1 && document.write("zgh666");
前面的为真,后面的语句执行,结果是zgh666
||
||和&&的规则大致相同,但一些地方相反
&&是寻找假,不找到假不停
而||是寻找真,不找到真不停
注意:||可以用于写兼容
例一:
div.onclick = function(e) {
var event = e || window.event;
}
三目运算符
条件判断?真:否 并且会返回该值
例一:
var name = 1 > 0 ? 2 +2 : 1+1;
result: 4
例二:
var name = 1 < 0 ? 2 +2 : 1+1;
result: 2
逗号操作符
使用逗号操作符时,先要加上(),防止出现优先级混乱
var a = (2, 3);
result: 2
逗号是一个计算符,有括号的加持,先计算逗号操作符,再把值赋给a
先:看前面的表达式,如果前面的表达式需要计算就先计算
然后再:如果后面表达式需要计算的话就计算后面的的表达式
都计算完后,把后面的表达式的结果返回回去
函数
高内聚,弱耦合
函数是变量,函数是引用值
函数的声明
关键字:function
固定格式
function test(){}
test是函数名,(参数),{函数体,即代码段}
起名:
函数名与变量名的起名方式差不多
开发规范:小驼峰原则(复合单词时),例如: theFirstName
函数表达式
函数表达式可以忽略它的名字
有两种函数表达式:
var test = function abc(){ /*命名函数表达式,adc仅仅只是函数名而已,不能通过
adc来调用这个函数,即写的这个名adc没什么用*/
......
}
var demo= function (){ /*匿名函数表达式(一般函数表达式
指的都是匿名函数表达式)*/
......
}
函数的参数
作用:聚合代码块;抽象规则(这一点使函数变得神奇)
在JavaScript中:形参可以任意多个,天生不定参,可以形参比实参多,反之亦可
arguments类数组
无论形参有没有把实参表示出来,实参都有地方去放
在每个函数里,都有一个隐式的东西,arguments类数组
function sum(a) {
/*arguments -- [11,55,66,88,9981] 实参列表,依次从左到右放入实参中,
函数中实参有多少,实参列表就有多少,不因为形参的某些变化而改变*/
......
}
sum(11,55,66,88,9981);
- 计算形参的个数用: arguments.length
- 计算实参的个数用: sum.length
映射规则
function sum(a,b) {
a = 2;
arguments[0] = 3;
console.log(a); //3
}
sum(1,2);
形参和实参不是同一个变量,但是有绑定规则,一个变了,另一个要跟着变(和c语言不同)
特殊:
当实参比形参少,给多的形参(b)赋值,但是arguments[1]的输出结果是undefined,此时的形参b与实参不映射
function sum(a,b) {
b = 2;
console.log(arguments[1]); //undefined
}
sum(1);
预编译(较难)
准备知识:
- imply global 暗示全局变量:即任何变量,如果未经声明就赋值,此变量就为全局对象所有
- 全局对象是window
a = 10; <=> window.a = 10;
- window就是全局的域
- 预编译发生在函数执行前的一刻
函数预编译四部曲:
- 创建AO对象
- 找形参和变量声明,将变量和形参名作为AO属性名,值为undefined
- 将实参与形参统一
- 在函数体里面找到函数声明,值赋予函数体
全局预编译
- 与函数预编译的规则基本一致
- 不过生成的对象是GO(对象GO就是window)
- 所以全局变量都是window上的属性
注意:for循环语句,if条件语句…不影响预编译,里面的内容该预编译就预编译
作用域(较难)
作用域精解:
- [[scope]]:每个js函数都是一个对象,对象中有些属性我们可以访问,例如:name;但有些不可以,这些属性仅供js引擎存取,[[scope]]就是其中一个。[[scope]]指的就是我们所说的作用域,其中存储了运行期上下文的集合。
- 作用域链:[[scope]]所存储的执行期上下文的对象的集合,这个集合呈链式链接,我们把这种链式链接叫做作用域链。
- 执行期上下文:例如:AO,GO,当函数执行的时候,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行时的环境,函数每次执行对应的执行期上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数(每次)执行完毕,他所产生的执行上下文被销毁。
- 查找变量:从作用域链的顶端依次向下查找(在哪个函数查找变量,就在那个函数的作用域链的顶端依次向下查找)
以下面代码例一来说明作用域
例一:
function a() {
}
var glob = 100;
a();
//s.[[scope]] --> 0:GO {}
函数刚刚被定义,它的[[scope]]就存了东西,此时只存储了一位,第0位存了GO,此时还没形成链
函数a被定义后,开始执行a();a函数产生执行期上下文AO,AO放到作用域链的顶端,占据第0位,而GO被放进第1位
特殊:函数里面定义函数的情况(与函数和全局的关系差不多),以例二来说明
例二:
function a(){
function b(){
var b = 234;
}
var a = 123;
b();
}
var glob = 100;
a();
a函数执行时:b函数定义,直接把函数a(上一级)的执行期上下文拿来
b函数执行时:生成自己的AO,放入作用域链[[scope]]的顶端
注意:
- 函数b访问的第1位的AO就是a函数产生的AO
- 函数b执行完后,它的第0位的AO被干掉,即第0位房间被清空,等待下一次函数b的执行,再建立一个自己的AO
- 如果函数a执行完,它的第0位的AO被干掉的同时,这个AO里面的函数b也永远没了,函数b的执行上下文也永远没了;然后函数a回到被定义的状态,等待再次被执行
- a函数再次被执行,又产生全新的执行上下文,然后放入作用域链[[scope]]的顶端,同时,又产生函数b的定义(全新的函数b),然后……
所以:
- 在哪个函数查找变量,就在那个函数的作用域链的顶端依次向下查找
- 所以外面的不能访问里面的变量,而里面的可以访问外面的变量
- 注意:只有函数被执行,里面的语句才会一条条被读,a函数执行后才有b函数的定义
闭包(较难,内部的函数被保存在外部时触发)
凡是内部的函数被保存在外部必定生成闭包
例一(粗略解释闭包):
function a() {
function b() {
var bbb = 234;
document.write(aaa);
}
var aaa = 123;
return b;
}
var glob = 100;
var demo = a();
demo();
a函数执行时,b定义(函数b的执行上下文继承了函数a的执行上下文)
所以,函数a执行完后,它的AO没有被销毁,只是与其的联系断掉了,还保留着与函数b的联系(也因为函数b定义被保存到外面,没有被干掉)
例二(闭包功能:累加器):
function a() {
var num = 100;
function b() {
num++;
document.write(num);
}
return b;
}
var demo = a();
demo(); //101
demo(); //102
例三(闭包功能:缓存):
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("apple");
eater.eat(); //i am eating apple
function test() {
var food = "apple";
var obj = {
eatFood: function(){
if (food != ""){
console.log("i am eating" + food);
food = "";
}
else{
console.log("there is nothing ! empty!");
}
},
pushFood: function(myFood){
food = myFood;
}
}
return obj;
}
var person = test();
person.eatFood(); //i am eating apple
person.eatFood(); //there is nothing ! empty!
person.pushFood("banana");
person.eatFood(); //i am eating banana
例四(可以实现封装,属性私有化):
function Deng (name,wife) {
var prepareWife = "xiao er";
this.name = name;
this.wife = wife;
this.divorce = function () {
this.wife = prepareWife;
}
this.changePrepareWife = function () {
prepareWife = target;
}
this.sayPrepareWife = function () {
console.log(prepareWife);
}
}
var deng = new Deng('deng', 'xiao yi');
deng.divorce();
deng.wife; //xiao er
deng.prepareWife; //undefined
经过闭包后,prepareWife这个变量对于这个对象变得很微妙
deng.prepareWife访问不到这个变量,但是它可以操作这个变量;表面上看prepareWife这个变量不是它自己的,但闭包永远跟随它,闭包相当于一个隐藏的区域;prepareWife这个变量变得像它的私有化变量似的(只有它自己能看到,别人看都看不到)
例五(模块化开发,防止污染全局变量,用来解决变量名冲突):
var name = "zzz";
var init = (function () {
var name = "ggg";
function callName() {
console.log(name);
}
return function() {
callName();
}
}())
init();
将全局要实现的功能放到了一个局部里面,使它们互相不污染
例六:
function test() {
var arr =[];
for (var i = 0; i < 10; i++){
arr[i] = function() {
document.write(i);
}
}
return arr;
}
var myArr = test();
for (var i = 0; i < 10; i++){
myArr[i]();
}
result: 打印出10个10(在函数被执行时才会读里面的语句,等函数被保存到外部后,开始执行函数里面的语句时,i的值已经是10)
function test() {
var arr =[];
for (var i = 0; i < 10; i++){
(function (j) { /*上面代码的基础上使用立即执行函数*/
arr[j] = function() {
document.write(j);
}
})(i)
}
return arr;
}
var myArr = test();
for (var i = 0; i < 10; i++){
myArr[i]();
}
result: 打印出0到9
例七(除了用return,还可以用全局变量实现闭包的效果):
var demo;
function test () {
var abc = 100;
function a () {
console.log(abc);
}
demo = a;
}
test();
demo();
闭包的危害:
当闭包函数被保存在外部时,将会生成闭包。闭包会导致原有作用域链不被释放,造成内存泄漏(即内存被占用)
立即执行函数(很有意思)
- 此类函数没有声明,在一次执行后即释放,适合做初始化工作
- 除啦写法,和执行完就销毁,其余特性和一般函数是一样的
立即执行函数很有意思的一点:不是特意规定的语法,是后来人们发现的,利用()即执行符号的特点
立即执行函数,官方给出两种写法:
- (function () {……}()) W3C 建议第一种
- (function () {……})()
- 不用函数名,左边的(写形参),右边的(写实参)
注意:只有表达式才能被执行符号()执行;
函数声明不是表达式,函数表达式是表达式
例一:
var test = function () {
console.log('...');
}();
这也是立即执行函数的一种写法
可以把函数声明变成表达式,加执行符号(),变成立即执行函数
例二:
-function () {
console.log('...');
}();
立即执行函数的写法还可以通过在函数声明前加正号或负号,使函数声明通过隐式转换变成表达式,再在后面加执行符号()
例三;
function test (a, b, c, d) {
console.log(a + b + c + d);
}(2, 3, 5, 9);
这样不会报错,但是也不会执行。系统把函数声明和 (2, 3, 5, 9) 看作是分开的, (2, 3, 5, 9) 不被看作执行符号
对象
对象的定义:
var deng = {
lastName : "Deng",
//属性名 用冒号连接 属性值 用逗号隔开
age : 40,
handsome : false //结尾处不用写逗号了
}
console.log(deng.lastName); //取值举例
deng.lastName = "old deng"; //赋值举例
补充:对象里面的函数叫做方法
var haha = {
name : "xixi",
age : 66,
health : 100,
smoke : function () {
console.log("i am smoking!cool!");
this.health--; /*在对象里面,this.health <==> haha.health */
},
drink : function () {
console.log("i am drink");
this.health++;
}
}
对象的定义方法有四种:对象自变量,构造函数(系统,自定义),Object.creat()
对象的增、删、改、查:
var haha = {
name : "xixi",
age : 66,
}
haha.wife = "xiaosan"; //增,注意要给属性值
haha.name = "gaga"; //改
delete haha.age; //删,要借助delete操作符
//注意中间有空格
console.log(haha.age); //undefined
/*当一个变量没定义就使用(除了typeof)会报错,
但是一个对象的属性没定义就打印,为undefined*/
delete
一旦经历了 var 的操作,所得出的属性 window 这种属性叫做不可配置的属性
不可配置的属性 delete不掉
属性的表示方法
- obj.prop (最常用的,内部原理:当你调用obj.prop时,会在内部隐式转换成obj[“prop”])
- obj[“prop”] (更加灵活)
法二的一个应用举例(把字符串和 + 的特点结合起来,实现属性的拼接):
var deng = {
wife1 : {name : "xiao yi"}, //对象的属性名是String类型的
wife2 : {name : "xiao er"},
wife3 : {name : "xiao san"},
wife4 : {name : "xiao si"},
wife5 : {name : "xiao wu"},
sayWife : function (num) {
return this['wife' + num];
}
}
对象的创建方法:
- var obj = {} plainObject 对象字面量/对象直接量
- 构造函数:系统自带的构造函数和自定义
构造函数
var obj = new Object();
Object()是系统自带的构造函数,这个构造函数可以批量生产对象,每个对象都一样,但是彼此独立
new Object() 能产生一个对象
var obj = new Object();
var obj = {};
这两种写法没有任何区别
系统自带构造函数创建的对象属性的添加可以通过外面添加
JavaScript语言中的对象,相比其他语言是非常灵活的
自定义构造函数
函数形式和一般函数完全一样,加new就可以生成对象
function Student (name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.grade = 2018;
}
var student1 = new Student("zgh", "18", "male");
注意:构造函数的命名规则:大驼峰(每个首字母都大写),使其与一般函数易于区别
构造函数内部原理(较难,隐式this对象)
- 在函数体最前面隐式的加上 this = {}
- 执行 this.xxx = xxx;
- 隐式的返回this
不过,函数必须加new才能成为构造函数,有了new,上面三个隐式步骤才能开始;没有new就是正常函数执行
一:逻辑最前面加上 var this = {} ,即隐式创建this对象,而this对象并不是空对象,诞生时应该是这样的 :
function Student (name, age, sex) {
/*var this = {_proto_:Student.prototype},如果在Student对象中
找不到他要的属性,就会通过_proto_属性去它的原型中找,所以_proto_的
作用是连接对象和它的原型的*/
this.name = name;
this.age = age;
this.sex = sex;
this.grade = 2018;
}
var student1 = new Student("zgh", "18", "male");
二:执行this ,即给this对象里面的属性传参赋值:
function Student (name, age, sex) {
/* var this = {
_proto_ : Student.prototype,
name : "zgh",
age : "18" ,
sex : "male"
}
this.name = name;
this.age = age;
this.sex = sex;
this.grade = 2018;
//return this;(三:隐式返回this对象)
}
var student1 = new Student("zgh", "18", "male");
注意,如果函数没有加new,this默认指向window
在对象里面的this指向对象名
做一个小改动,在构造函数最后显式的返回一个空对象(或原始值):
function Student (name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.grade = 2018;
//return {}; 使这个构造函数创建的对象是一个空对象,因为显式优于隐式
/*return 原始值; 那么这个捣乱是不成功的,因为
函数只要加了new,返回的必须是对象,即使写了 返回 原始值 ,也会被忽略掉,
强制返回this对象;
有new了就不可能返回原始值*/
}
var student1 = new Student("zgh", "18", "male");
原型(较难,描述的就是继承关系)
- 定义:原型(prototype)是function对象的一个属性,它定义了构造函数制造出的对象的公有祖先。通过该构造函数产生的对象,可以继承该原型的属性和方法。原型也是对象
- 利用原型特点和概念,可以提取共有属性
- 对象如何查看原型——隐式属性
_proto_
- 对象如何查看对象的构造函数——
constructor
原型的定义:
形象理解:若人是对象,则原型是人的祖先
prototype(原型)是系统自带的属性
以下面代码来说明:
//Person.prototype 原型
//Person.prototype {} 祖先
function Person () {
}
var person = new Person();
Person.prototype 是函数刚刚出生,就已经被系统定义好了
Person.prototype是一个对象
Person.prototype = {} 可以看作一个’空’对象
Person.prototype = {} 可以理解为 Person 的构造函数,构造出的对象的爸爸
本来Person.prototype = {} 是’空’对象,若在这个’空’对象里面加属性:
Person.prototype.name = "hehe";
function Person () {
}
var person = new Person();
console.log(person); // Person {}
console.log(person.name); //"hehe"
name属性不是Person对象的属性,但是是它的原型的属性,所以可以直接调用,
原型描述的就是继承关系
注意:若对象及其原型有相同属性,调用这个属性时,用对象自己的属性
Person.prototype.name = "hehe";
function Person () {
this.name = "haha";
}
var person = new Person();
console.log(person.name); //"haha"
用于提取公有属性:
原型的应用(把公有的代码提取出来放在原型里)
一构造一个工厂构造函数为例:
function Car (color, owner) {
this.owner = owner;
this.color = color;
this.carName = "BMw";
this.height = 0.61818;
this.lang = 1;
}
var car = new Car('red', 'zgh');
这样写看似没有什么问题
但是这种流程化的东西,每一个用这个构造函数生成对象时,都要执行像this.height = 0.61818……这类构造函数中的不变属性,都要执行一遍
这就是问题所在,这是代码的冗余(每次创建Car对象都要执行许多一模一样的代码)
解决办法:用原型,继承
Car.prototype.carName = "BMw";
Car.prototype.height = 0.61818;
Car.prototype.lang = 1;
function Car (color, owner) {
this.owner = owner;
this.color = color;
}
var car = new Car('red', 'zgh');
这样简化代码
原型的增、删、改、查:
原型的增、删、改(除了查)只能通过原型本身来进行操作,其子孙无法操作
删:
用delete (在控制台中删除一个本来就没有的属性,会返回true)
原型的另一种设置方法:
Car.prototype.carName = "BMw";
Car.prototype.height = 0.61818;
Car.prototype.lang = 1;
function Car () {
}
var car = new Car();
Car.prototype = {
carName = "BMw";
height = 0.61818;
lang = 1;
}
function Car () {
}
var car = new Car();
第二种方法中的代码要放在有new的语句之前(理解这句话,请看下面的“几个小例子来理解Person.prototype.name = “haha” 和 Person.prototype {name : “haha”} 的不同”)
这两种设置方法一样
后者的写法更加简便
注意:刚出生的原型理论上好像是’空’对象
function Car () {
}
var car = new Car();
console.log(Car.prototype); //Object {}
若在控制台中 可以把 Object {} 展开
Object {
constructor : Car (),
_proto_ : Object //而且这两行代码在控制台中(是浅粉色的字体,代表隐式的)
//还可以继续展开
}
_proto_
:
小知识:有一种命名规则,在你开发的时候,不希望你的同事用方法访问某个属性时,一般这个属性以下划线_ 开头,通过名字告诉同事,你不要用
_proto_
的作用是连接对象和它的原型的(具体讲解请看:构造函数的内部原理)
_proto_
的属性值也可以手动修改:
Person.prototype.name = "hehe";
function Person () {
}
var obj = () {
name : "haha"
}
var person = new Person();
console.log(person._proto_); //Object {name : "hehe"}
person._proto_ = obj;
console.log(person._proto_); //Object {name : "haha"}
console.log(person.name); //"haha"
几个小例子来理解Person.prototype.name = “haha” 和 Person.prototype {name : “haha”} 的不同:
例一:
Person.prototype.name = "hehe";
function Person () {
}
Person.prototype.name = "haha";
var person = new Person();
console.log(person.name); //"haha"
例二:
Person.prototype.name = "hehe";
function Person () {
}
var person = new Person();
Person.prototype.name = "haha"; //这两行代码 和上面代码的顺序相反
console.log(person.name); //"haha"(顺序不影响结果)
例三:
Person.prototype.name = "hehe";
function Person () {
}
var person = new Person();
Person.prototype = {name : "haha"}
console.log(person.name); //"hehe"
而例三的结果为何不是”haha”,而是”hehe”?
解析:先用一个例子来理解Person.prototype {name : “haha”}
var obj = {name : "a"}
var obj1 = obj;
obj = {name : "s"}
console.log(obj1); //Object {name : "a"}
console.log(obj); //Object {name : "s"}
然后再根据构造函数内部原理来理解:
Person.prototype.name = "hehe";
function Person () {
//var this = {_proto_ : Person.prototype}
}
var person = new Person();
Person.prototype = {name : "haha"}
所以Person.prototype的指向空间改了,但是_proto_
指向的空间没有改
而Person.prototype.name = “haha” 是修改_proto_
和Person.prototype指向的同一空间的属性值,此两者指向的空间依旧相同
例四:
Person.prototype.name = "hehe";
function Person () {
}
Person.prototype = {name : "haha"}
var person = new Person();
console.log(person.name); //"haha"
解析:与例四不同的原因是,例三的 Person.prototype = {name : “haha”} 在构造函数内部原理执行后才执行,
而例四的 Person.prototype = {name : “haha”} 把 Person.prototype.name = “hehe”; 修改后才执行构造函数的内部原理
constructor(构造器):
function Car () {
}
var car = new Car();
console.log(car.constructor); //function Car(){}
//constructor属性可以返回构造出来的对象 的 构造函数
constructor属性值可以手动修改:
function Person () {}
Car.prototype = {
constructor : Person
}
function Car () {}
var car = new Car();
console.log(car.constructor); //function Person()
原型链
- 如何构成原型链?
- 原型链上属性的增删改查
- 绝大多数对象的最终都会继承自Object.prototype
- Object.prototype(原型);
一( 如何构成原型链?):
Grand.prototype.lastName = "deng"; //Grand.prototype 还不是原型的头
function Grand () {
}
var grand = new Grand();
Father.prototype = grand;
function Father() {
this.name = "baobei";
}
var father= new Father();
Grand.prototype = father;
function Son() {
this.hobbit = "tangtou"
}
var son = new Son();
console.log(son.hobbit); //"tangtou"
console.log(son.name); //"baibei"
console.log(son.lastName); //"deng"
原型链以_proto_
为链接点,访问顺序是由近到远
并且 Grand.prototype 还不是原型的头
Grand.prototype还有属性_proto_ : Object
,在控制台中还可以展开,里面有 toString() 属性
而且,Object.prototype 是所有对象的最终原型
二(原型链的增删改查):
原型链的增删改查基本上和原型的增删改查一致
查,就是由近到远的访问次序
增删改,只能通过原型本身操作,不能由子孙来操作
涉及原型链的计算:
function Grand () {
}
var grand = new Grand();
Father.prototype = grand;
function Father() {
this.num = 100; //father对象中 有 num属性
}
var father= new Father();
Grand.prototype = father;
function Son() {
//此时,son对象中 没有 num属性
}
var son = new Son();
son.num ++; //即:son.num = son.num +1;(相当于son对象增加了一个属性num,
// 然后被赋值(100 +1))
console.log(son.num); //101
console.log(father.num); //100
注意:原型链中涉及 this 的情况
Person.prototype = {
name : "a",
sayName : function(){
console.log(this.name);
}
}
function Person () {}
var person = new Person();
person.sayName(); //"a"
sayName中的this指向是:谁调用的这个办法,this就指向谁
Person.prototype = {
name : "a",
sayName : function(){
console.log(this.name);
}
}
function Person () {this.name = "b";}
var person = new Person();
person.sayName(); //"b"
person.prototype.sayName(); //"a"
加一个:
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.prototype.height); //100
有一个小问题:var obj = {};
这种创建对象的方法,创建的对象有没有原型?
答案是肯定有的
var obj = {};
和var obj = new Object();
这两种方法创建的对象是一模一样的
var obj = {};
的原型就是Object
这两种方法的优劣:
- 第一种(自变量的写法)更简单,常用
- 第二种极为少见(麻烦又没什么用)
Object.creat():
Object.creat()可以用来创建对象
并且括号里必须写原型(原型可以自己指定)
var obj = {name : "smile", age : 666};
var obj1 = Object.creat(obj);
这是更灵活创建对象的方法
Person.prototype.name = "smile";
function Person () {}
var person = Object.creat(Person.prototype); //var person = new Person();
绝对大多数对象都会继承自Object.prototype
为什么不是所有的?
特例:Object.creat(null)
知识点:Object.creat() 的括号里必须填写对象(数组也可以算作对象)或null,不然会报错
然而Object.creat(null) 构造的对象是没有原型的
可以试一下 人为的给这种对象设置_proto_
属性,但是并没有继承特性
所以:_proto_
是系统设置的隐式的属性,人为的添加是不管用的
对toString()的简略探究:
我们知道了Object.prototype 是所有对象的最终原型,
而且 Object.prototype 有toString()属性
所以可以理解了:undefined和null没有toString()属性(因为undefined和null不是对象,也不可以经过包装类)
对于Boolean:console.log(true.toString()); //"true"
对于Number:
不能直接写 123.toString() ,会报错的(因为对象中 . 是调用一个方法,但数学计算中 . 的优先级别更高,这么写,系统会优先识别浮点型,然后发现浮点后面不是数字,所以就报错)
要这么写:
var num = 123;
console.log(num.toString()); //"123"
对于对象(这个结果有些不一样):
var obj = {};
console.log(obj.toString()); //"[object Object]"
和我们想的不太一样,不和上面那些例子一样,没有返回"{}"
还有:
console.log(Object.prototype.toString.call(123)); //"[object Number]"
console.log(Object.prototype.toString.call(true));//"[object Boolean]"
toString() 还有一个很有意思的一点,当调用document.write()时,往页面打印东西的时候,会调用它的隐式的toString() 方法
call,apply
极小极小的知识点,应用非常广
作用:改变this指向
区别:后面的穿参形式不同
call的根本作用是改变this指向(把this赋值给括号里第一个参数)
任何一个方法都可以 .call
一:
其实.call才是一个方法执行的真实面目
test() 等价于 test.call()
二:
但是test.call() 的括号里写东西的时候,()里传的东西可了不得
当东西传进去后,call会引导test发生天翻地覆的变化
function Person(name, age) {
//obj = this;
this.name = name;
this.age = age;
}
var person = new Person('deng', 100);
var obj = {}
Person.call(obj);/*call()会让Person里面所有预设的this都赋值
给obj(把this赋值给括号里第一个参数obj)*/
三:
Person.call(obj)这种写法需要传参怎么办?
从第二位开始写就可以
function Person(name, age) {
//obj = this;
this.name = name;
this.age = age;
}
var person = new Person('deng', 100);
var obj = {}
Person.call(obj, 'cheng', 666);
console.log(obj);//Object {name : "cheng", age : 666}
console.log(person);//Person {name : "deng", age : 100}
Person.call(obj)执行的时候,让Person里面的this 赋值给了 obj
这不是继承!
这样,通过call的方法,让Person来封装obj(借助Person的环节,制造obj的属性)
借用了别人的函数,构造了自己的对象
call的进一步引申:
在部门开发的时候,讲究快,准,狠
例一(当某个功能完美覆盖另一个功能):
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
function Student(name, age, sex, tel, grade) {
this.name = name;
this.age = age;
this.sex = sex;
this.tel = tel;
this.grade = grade;
}
var stuent = new Student('zgh', 666, male, 110, 2018);
用call后:
function Person(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
function Student(name, age, sex, tel, grade) {
Person.call(this, name, age, sex);
this.tel = tel;
this.grade = grade;
}
var stuent = new Student('zgh', 666, male, 110, 2018);
例二(当某个功能完美覆盖许多其他功能):
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);
}
/*function Car(WheelSize, style, c, sitColor, height, width, len) {
Wheel.apply(this, [WheelSize, style]);
Sit.apply(this, [c, sitColor]);
Model.apply(this, [height, width, len]);
}*/
var car = new Car(100, '花里胡哨的', '假皮', 'green', 1800, 1900, 4900);
apply和call的区别:
1.拼写不同
2.后面传的参数类型不同
call() 里,从第二位开始可以一位一位传实参(需要把实参按照形参的个数传进去)
但是apply() 里,只能在第二位传一个实参,并且实参必须是数组形式(需要传一个arguments)
继承(圣杯模式,最完美的继承模式)
function inherit(Target, Origin) {
function F(){};
F.prototype = Origin.prototype;//这两行这样写,是为了避免改动target的原型
Target.prototype = new F(); //而也把origin的原型也改了
//而且这两行代码不能颠倒,不然不能实现继承的效果
Target.prototype.constructor = Target;//如果不写这一行,
//Target._proto_ --> new F()._proto_ --> Origin.prototype
//则 Target.constructor 指向 function Origin(){}
Target.prototype.uber = Origin.prototype;
//这一行代码作用是:知道 Target 真正继承自谁
}
function Father () {
}
function Son () {
}
inherit(Son, Father);
var son = new Son();
var father = new Father();
以上写法是最通俗的圣杯模式的写法,不够高大上
雅虎给外界提供的一个YUI3库中,库中封装好了许多功能
其中的inherit功能是这么写的:
(注意:return后面接的是函数引用)
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;
}
})();
//这种写法,运用了闭包:私有化属性
//这种高大上的写法,把中间变量F变成私有化,更好
对象的枚举(enumeration,即遍历)
- for in
- hasOwnProperty
- in (使用概率极其的低)
- instanceof
正式知识点之前的引入:
数组的遍历可以这样写
var arr = [1, 2, 8, 9];
for(var i = 0; i <= arr.length; i++) {
console.log(arr[i]);
}
但是对象的遍历怎么办呢?(for循环不行了,因为你不知道对象有多少个属性)
对像的遍历要借助 for in 循环
var obj = {
name : zgh,
age : 500,
sex : "male",
height : 170,
weight : 80
}
for (var prop in obj) {
console.log(prop); //遍历对象的属性名
}
//for in循环是通过对象的属性(方法)的个数来控制循环圈数
//并且会在每一次循环时,把对 象 的 属 性名(方法名)放在prop里面
//即:在每个循环时prop代表属性名
//(prop只是人为设置的一个变量,可以起其他名)
var obj = {
name : zgh,
age : 500,
sex : "male",
height : 170,
weight : 80
}
for (var prop in obj) { //对象的属性值的遍历
console.log(obj.[prop]);
//注意:[prop]不要写成['prop']
}
//prop是String类型的
//注意一点:为什么对象属性值的遍历不使用 obj.prop
/*
因为这样写,系统会把 prop 当作一个变量,在obj对象中找不到
这个属性值,会返回 undefined
*/
//所以在对象的枚举涉及属性值时,不要用 .
for in 循环还有一个问题,会把它的原型上的属性也拿出来
注意:for in 循环,只会把人为添加的原型上的属性拿出来,
比如原型链的顶端 Object.prototype 上系统设置的属性是拿不出来的
如果对象的遍历时不想要原型上的属性,使用 hasOwnProperty()
作用:验证是否是自己本身的属性,返回的是Boolean值
var obj = {
name : zgh,
age : 500,
sex : "male",
height : 170,
weight : 80,
_proto_ : {
lastName : "zhang" //lastName是obj原型链上的属性
}
}
for (var prop in obj) {
if(obj.hasOwnProperty(prop)){
console.log(obj.[prop]);
}
}
in 操作符:
“使用率极其的低”
作用:判断对象能不能访问到这个属性
在控制台中这样写:'height' in obj
,返回值是Boolean值
注意:这里 'height'
不能写成 height
,因为会被系统认为是某个变量
instanceof 操作符:
A instanceof B;
//作用:看A对象的原型链上有没有B的原型,返回的是Boolean值
深度克隆(只考虑原始值,数组和对象)
先说一下浅度克隆:
var obj = {
name : 'zgh',
sex : ''male,
age : 500
}
var obj1 = {};
function clone(Origin, Target) {
var Target = Target || {};//从用户角度,加上这一行代码
for(var prop in Origin){
Target[prop] = Origin[prop];
}
}
clone(obj, obj1);
//这种方法在只克隆原始值的时候,是好使的
//然而,存在引用值时,克隆的是地址,无法达到克隆的目的(克隆后对象与原对象要无关联)
//即,这种写法,无法成功克隆引用值,改动一个,另一个也会跟着改动
深度克隆(只考虑原始值,数组和对象):
function deepClone(Origin, Target){
var Target = Target || {};
var toStr = Object.prototype.toString;
var isArr = "[object Array]"
for(var prop in Origin){
if(Origin.hasOwnProperty(prop)){
if(Origin[prop] != 'null' && typeof(Origin[prop]) == 'object'){
target[prop] = toStr.call(Origin[prop]) == isArr ? [] : {};
deepClone(Origin[prop], Target[prop]);
}
else{
Target[prop] = Origin[prop];
}
}
}
return Target;
}
封装type
要求:
- typeof([]) – array
- typeof({}) – object
- typeof(function) – object
- typeof(new Number()) – number Object
- typeof(123) – number
思路:
- 分两类:原始值和引用值
- 区分引用值
- 注意:typeof(null) = ‘object’
function type() {
var ret = typeof(atrget);
var template = {
"[object Array]" : "array",
"[object Object]" : "object",
"[object Number]" : "number-object",
"[object Boolean]" : "boolean-object",
"[object String]" : "string-object"
}
/*旁批:template对象的这种写法称为工具方法,
提前写好需要用的方法,
需要用到时直接调用这个对象*/
if(target == null){
return null;
}
if(ret == 'object'){
var str = Object.prototype.toString.call(target);
return template[str];
}else{
return typeof(target);
}
}
包装类(较难,原始值调用属性和方法时触发)
Car.prototype.carName = "BMw";
Car.prototype.height = 0.61818;
Car.prototype.lang = 1;
function Car (color, owner) {
this.owner = owner;
this.color = color;
}
var car = new Car('red', 'zgh');
- new String()
- new Boolean()
new Number()
注意:
原始值是不能有属性和方法的
- 属性和方法是对象独有的东西
数字不一定是原始值,只有原始值数字才是原始值
在JavaScript中对于变量的灵活程度是空前的高
var num = 123; //这是数字(原始值数字)
var num = new Number(123); /*这也是数字(对象数字,可以加属性和方法)*/
而且对象数字还有数字特性,依旧可以参与运算,但是它运算完后,变成了原始值数字,不再是对象了
字符串,布尔类型和数字完全一致
而且,字符串对象可以像对象一样操作
(小知识:布尔创建了真和假,这是计算机的底层原理)
注意:undefined,null不可以设置属性和方法
包装类的前提准备:
原始值是坚决不能有属性和方法的,但是
var str = "abcd";
console.log(str.length); //4
那么,length属性哪儿来的呢?
还有:
var str = "asdf";
str.a = 'a';
console.log(str.a); //undefined
不能给原始值赋属性值,但为什么不报错?而且属性a的属性值也没有赋进去
其实这些都是因为它们经历了一个过程——包装类
1.var num = 4;
2.num.len = 666;
//new Number(4).len = 666; delete
3.console.log(numl.len); //new Number(4).len ,result:undefined
以上面代码来解释:
第2行:原始值调用属性时,会隐式创建对象,例如:new Number(4).len = 666,然后delete掉;
第3行:下一次再访问num.len时,系统又会重复上面所写的过程,新创建的Number对象和原来创建的Number对象不是同一个,它们不一样,新创建的Number对象,因为只访问了num.len,此时是没有赋属性值的,所以打印的结果是undefined
数组
数组的定义:
- 数组字面量
var arr = [];
- 构造方法定义数组
var arr = new Array();
- 这两种写法差不多,唯一的区别是:第二种方法里只传一个参数的时候
- 数组能用的一切方法都来源于 Array.prototype
一:
var arr = [];
//这种方法,想怎么写就怎么写
//里面只写逗号都没问题,这种叫稀松数组(不是里面每一位都有值)
var arr1 = [,];//[undefined * 1]
var arr2 = [,,];//[undefined *2]
var arr3 = [1,,1];//[1, undefined, 1]
二:
var arr = new Array(1,2,3,4,5);//[1,2,3,4,5]
//和第一种方法的区别在于这:
var arr1 = [10]; //[10]
var arr2 = new Array(10); //[undefined * 10]
//长度为10的空数组就是这么创建的
/*
第二种方法只传一个参数时, new Array() 不会把这一个参数当作值,
而是当作数组长度
*/
//所以, new Array() 中只传一个参数时,必须是正整型
var arr3 = new Array(10.2);/*Uncaught RangeError: Invalid array length*/
数组的读和写:
数组的读和写,规则极其松散,没什么可以报错的
var arr = [];
console.log(arr[10]);//undefined
//数组越界访问都不报错
arr[10] = 'zgh';
console.log(arr);//[undefined *10, 'zgh']
console.log(arr.length);//11
为什么呢?
(因为JavaScript中数组是基于对象的,数组是特殊的对象)
补充:数组的遍历可以使用 for in 循环
数组的常用方法(es3.0)
数组的方法分为:es3.0, es5.0, es6.0
es3.0的数组方法,是数组最重要的一些方法
- 改变原数组:push, pop, unshift, shift, reverse, sort, splice
- 不改变原数组:concat, join –> split, toString, slice
- 不改变原数组的方法:都要定义参数来接收结果
push,pop,unshift,shift,reverse
- push(从数组的最后一位开始添加数据,返回的是数组长度)
- pop (把数组最后一位剪切,返回剪切值)
- unshift (从数组第一位开始添加,返回数组长度)
- shift (把数组第一位剪切,返回剪切值)
- reverse (把数组逆转,返回逆转后的数组)
push
//push(从数组的最后一位开始添加数据,返回的是数组长度)
var arr = [];
console.log(arr.push(10));//1
conole.log(arr);//[10]
console.log(arr.push(11));//2
console.log(arr);//[10, 11]
console.log(arr.push(9));//3
console.log(arr);//[10, 11, 9]
//还可以同时添加多个:
console.log(arr.push(1,1,1,3,5));//8
console.log(arr);//[10, 11, 9, 1, 1, 1, 3, 5]
pop
//pop (把数组最后一位剪切,返回剪切值)
var arr = [1, 2, 3];
console.log(arr.pop);//3
console.log(arr);//[1, 2]
//注意:这个方法不需要传参,如果传参也会被忽略
unshift
unshift (从数组第一位开始添加,返回数组长度)
var arr = [1, 2, 3];
console.log(arr.unshift(0));//4
console.log(arr);//[0, 1, 2, 3]
//可以添加多个
console.log(arr.unshift(-2, -1));//6
console.log(arr);//[-2, -1, 0, 1, 2, 3]
shift
shift (把数组第一位剪切,返回剪切值)
var arr = [1, 2, 3];
console.log(arr.shift());//1
console.log(arr);//[2, 3]
reverse
reverse (把数组逆转,返回逆转后的数组)
var arr = [1, 2, 3];
console.log(arr.reverse);//[3, 2, 1]
console.log(arr.reverse);//[1, 2, 3]
splice
splice()里面的参数:
- 第一位表示从第几位开始(可以是负数,代表倒数第几个)
- 重点注意:参数第一位表示的第几位,是指数组的下标(下标是从零开始的)
- 第二位表示剪切多少长度(补充:返回值是剪切的部分)
- 第三位表示在切口处开始添加新的数据,切口处(光标)在第几位上数据的左侧紧挨
//只写前两个参数
var arr = [1, 1, 2, 2, 3, 3];
console.log(arr.splice(1,2));//[1, 2]
console.log(arr);//[1, 2, 3, 3]
//写三个参数及以上
var arr = [1, 1, 2, 2, 3, 3];
console.log(arr.splice(1,1,0,0,0));//[1]
console.log(arr);//[1, 0, 0, 0, 2, 2, 3, 3]
//可以用于在数组中插入数据
var arr = [1, 2, 3, 5];
arr.splice(3,0,4);
console.log(arr);//[1, 2, 3, 4, 5]
sort
这个功能较为复杂
//sort() 可以实现给数组升序排序
//(但却是按照ASCII码来排序的)
var arr = [1, 3 ,4 ,0 ,-1, 9];
arr.sort();
console.log(arr);//[-1, 0 , 1, 3, 4, 9]
var arr1 = [1, 3, 5, 10, -1];
arr1.sort();
console.log(arr1);//[-1, 1, 10, 3, 5]
/*这虽然使字符串也可以排序,但是:
这不是我们想要的排序*/
不过我们可以改变sort的排序功能
- sort() 给我们留下接口,在()中可以填写函数,自己写函数体,实现自己想要的排序功能
arr.sort(function (a, b){...return ...});
()中的函数体必须写两个形参
用这个方法时,sort() 里的函数会被调用很多次:
第一次调用函数的时候,会把数组的第零位和第一位通过形参传进函数里面
- 第二次调用函数的时候,会把数组的第一位和第二位通过形参传进函数里面
- 。。。
- 直到最后调用函数,传最后两个参数
看返回值:
- 当返回值为负数的时候,那么前面的数放在前面
- 为正数,那么后面的数在前面
- 为0,不变
//真正的数字升序排列:
var arr = [1, 3, 5, 10, -1];
arr.sort(function (a, b){
if(a > b){
return 1;
}
else{
return -1;
}
});
console.log(arr);//[-1, 1, 3, 5, 10]
上面代码简化版:
var arr = [1, 3, 5, 10, -1];
arr.sort(function (a, b){
return a - b;
});
//总结:
//return a - b; 升序
//return b - a; 降序
concat,toString,slice,join,split(这五个方法不会改变原数组)
- concat(把两个数组拼接在一起,并且不会影响原来的两个原数组)
- toString(把数组里面的内容变成字符串)
- slice(有两个参数:从该位开始剪切,剪切到该位; 只有一个参数:从该位开始剪切到最后;无参数:整个剪切 )
- join(传的参数必须是字符串类型的,把数组的每一位都用传的参数连接起来,返回的是字符串类型的;注意:如果不传参数,由“,”连接)
- split(是字符串的方法,但是可以和数组的方法可逆;按传的参数把字符串拆分成数组)
concat
//concat(把两个数组拼接在一起,并且不会影响原来的两个原数组)
var arr = [1, 2, 3, 4, 5];
var arr1 = [6, 7];
var newArr = arr.concat(arr1);
console.log(newArr);//[1, 2, 3, 4, 5, 6, 7]
//拼接在后面的数组放在()里
toString
//toString(把数组里面的内容变成字符串)
var arr = [1, 2, 3, 4, 5, 6, 7];
var newStr = arr.toString();
console.log(newStr);//"1,2,3,4,5,6,7"
slice
/*slice(有两个参数 :从该位开始剪切,剪切到该位;
只有一个参数:从该位开始剪切到最后;
无参数 :整个剪切 )
//两个参数
var arr2 = [1, 2, 3, 4, 5, 6];
var newArr2 = arr.slice(1,3);
console.log(newArr2);//[2, 3, 4]
//一个参数
var arr1 = [1, 2, 3, 4, 5, 6];
var newArr1 = arr.slice(1);
console.log(newArr1);//[2, 3, 4, 5, 6]
//无参数(在类数组中有用)
var arr = [1, 2, 3, 4, 5, 6];
var newArr = arr.slice(1);
console.log(newArr);//[1, 2, 3, 4, 5, 6]
//注意:参数可以填写负数
var arr = [1, 2, 3, 4, 5, 6];
var newArr = arr.slice(-4);
console.log(newArr);//[3, 4, 5, 6]
join
join 这个方法非常有用!
/*join(传的参数必须是字符串类型的,
把数组的每一位都用传的参数连接起来,
返回的是字符串类型的;
注意:如果不传参数,由“,”连接)*/
var arr = [1, 2, 3, 4, 5, 6];
var newStr = arr.join("-");
console.log(newStr);//"1-2-3-4-5-6"
split
//split(是字符串的方法,但是可以和数组的方法可逆;
按传的参数把字符串拆分成数组)
var arr = [1, 2, 3, 4, 5, 6];
var str = arr.join("-");
console.log(str);//"1-2-3-4-5-6"
console.log(str.split("-"));//["1", "2", "3", "4", "5", "6"]
console.log(str.split("4"));//["1-2-3-", "-5-6"]
数组去重
//以具体目的来讲:
var arr = [1, 1, 1, 2, 2, 2, 3, 3, 3];
arr.unique() --> [1, 2, 3]
//还有,将方法写在数组原型链上
方法:哈希方式(将数组的属性值当作对象的属性名,来去重)
var arr = [1, 1, 1, 2, 2, 2, 3, 3, 3];
Array.prototype.unique(){
var temp = {},
arr = [],
len = this.length;
for(var i = 0; i < len; i++){
if(!temp[this[i]]){
this[i] = "haha";//注意这里赋值,不能是Boolean值为false的值,其余都行
arr.push[this[i]];
}
}
return arr;
}
console.log(arr.unique());//[1, 2, 3]
类数组
- 可以利用属性名模拟数组的特性
- 可以动态的增加length属性
- 如果强行让类数组调用push方法,则会根据length属性值的位置进行属性的扩充
前提引入:
function test () {
console.log(arguments);
arguments.push();//UncaughtTypeError: arguments.push is not a function at test
//arguments长得像数组;但却是类数组,没有push方法;
}
test(1, 2, 3);
类数组是从对象来的:
- 属性要为索引(数字)属性
- 必须有length属性
- 最好加上push方法 -
//对象中属性为索引(数字)属性
var obj = {
"0" : 'a',
"1" : 'b',
"2" : 'c'
}
console.log(obj[0]);//"a" 这样写很像访问数组了
console.log(obj);//Object{0:"a",1:"b",2:"c"}
var obj = {
"0" : 'a',
"1" : 'b',
"2" : 'c',
length : 3 //对象中加 length属性
}
console.log(obj);//Object{0:"a",1:"b",2:"c",length:3}
var obj = {
"0" : 'a',
"1" : 'b',
"2" : 'c',
"length" : 3,
"push" : Array.prototype.push //再加上push方法
}
obj.push("d");
console.log(obj);//Object{0:"a",1:"b",2:"c",3:"d",length:4}
/*加了length和push方法后,这个对象可以
调用数组的push方法,而且length也动态的增加了*/
var obj = {
"0" : 'a',
"1" : 'b',
"2" : 'c',
"length" : 3,
"push" : Array.prototype.push,
"splice" : Array.prototype.splice //再加上splice方法后,
//类数组就调用时长得和数组一样了
}
console.log(obj);//["a", "b", "c"]
小试牛刀:
当年阿里巴巴出了这样一道题(注意细节!):
var obj = {
"2" : 'a',
"3" : 'b',
"length" : 2,
"push" : Array.prototype.push
}
obj.push('c');
obj.push('d');
console.log(obj);//???
//提示:弄懂push方法是如何实现的
//答案是:Object{2:"c",3:"d",length:4}
push方法:
Array.prototype.push = function() {
for (var i = 0; i <= arguments.length; i++){
this[this.length] = arguments[i];
this.legth++;
}
return this.length;
}
//说以:类数组的关键点在length上,length决定了在那一位push东西
类数组用处极大,既能像数组那样使用,又能像对象那样使用:
var obj = {
"0" : 'a',
"1" : 'b',
"2" : 'c',
"length" : 3,
name : "zgh",
age : 500,
"push" : Array.prototype.push,
"splice" : Array.prototype.splice
}
console.log(obj);//["a", "b", "c"]
cosnole.log(obj.length);//3
cosnole.log(obj.name);//"zgh"
cosnole.log(obj.age);//500
类数组很有用
日后所学的DOM方法所能生成的所有像数组一样的东西都是类数组
高级编程,基本全是类数字
小知识点补充:
switch语句(break,continue)
var n = true;
switch(n) {
case "a":
console.log('a');
case "b":
console.log('b');
case "c":
console.log('c');
}
switch语句中的case后带的内容随意(数字,字符,true……)
switch语句中有一个很蛋疼的特点: n = “a”;时,switch语句中不仅会执行case “a”中的语句,后面的case语句也执行
为了解决这个问题,善用break
var n = true;
switch(n) {
case "a":
console.log('a');
break;
case "b":
console.log('b');
break;
case "c":
console.log('c');
break;
}
break(终止循环),switch可以看作一种循环吧
break只能用于循环语句中,不然会报错
补充:continue(继续,终止本次循环,执行下一次循环)
parseInt()用于去掉像素值中的px
parseInt(),括号内的数据,从第一位数字位看起,看到非数字位停止(若第一位不是数字,则返回NaN)
var demo = "123px";
var num = parseInt(demo);
console.log(typeof(num) + ":" + num);
result: Number : 123
parseInt(),toString()可以用于进制转换
parseInt( num, radix) 将num以radix为基底,转化为十进制数
num.toString(radix) 将十进制数num转换为radix进制数
访问未定义变量不报错的情况:typeof(未定义的变量)
一般,如果访问没定义的变量,会报错
但有一极特殊的情况,
当且仅当:未定义的变量放入typeof()中不会报错
result: undefined(是字符串类型的)
四舍五入,toFixed()
toFixed(),括号里是多少,就保留多少有效数字,并且会四舍五入
var num = 15.3215698541;
document.write(num.toFixed(3));
result: 15.322
用arguments求和
var result = 0;
for(var i = 0; i < arguments.length; i++){
result += arguments[i];
}
sum(...);
arguments.callee 和 caller的区别
arguments.callee:
- arguments是类数组
- arguments上的属性只有callee 和 length
- arguments.callee指向的是函数的引用
作用举例:
用立即执行函数,计算100的阶乘(使用递归时,需要知道函数名(函数引用))
var num = (function (n) {
if (n == 1) {
return 1;
}
return n * arguments.callee(n - 1);
})(100);
注意:arguments在哪个函数,arguments.callee就指向哪个函数
caller:
- caller是函数上的属性
- 该属性保存着调用当前函数的函数
- 如果没有父函数,则为null
- 一般在笔试题上和callee一起考
作用举例:
function test () {
demo();
}
function demo () {
console.log(demo.caller);//function(){demo();}
}
调用字符串部分内容
法一:字符串是基于数组写的,调用字符串的部分内容,可以像数组一样调用
法二:调用字符串可以使用 变量名.charAt()
例一:
var str = "123";
console.log(str[0]); //"1"
例二:
var str = "123";
console.log(str.charAt(0));//"1"
console.log(str.charAt(1));//"2"
解决变量名冲突
变量私有化(闭包的功能)
var name = "zzz";
var init = (function () {
var name = "ggg";
function callName() {
console.log(name);
}
return function() {
callName();
}
}())
init();
将全局要实现的功能放到了一个局部里面,使它们互相不污染
实现一个方法的连续调用
如何实现链式调用模式(模范jQuery)
小补充:jQuery是一个非常强大的库,是一帮老大哥们写的非常复杂的JavaScript文件包,文件包留出许多方法的接口,我们可以用它的方法实现我们的功能
var deng = {
a :function () {
console.log("A");
},
b :function () {
console.log("B");
},
c :function () {
console.log("C");
}
}
deng.a().b(); /*这样是会报错的,因为deng.a()会被系统返回undefined,然后
deng.a().b()会被看做undefined.b()*/
怎么才能实现,对象直接一个接一个这样调用属性(巧用return)
var deng = {
a :function () {
console.log("A");
return this;
},
b :function () {
console.log("B");
return this;
},
c :function () {
console.log("C");
return this;
}
}
deng.a().b().c();
判断变量是对象还是数组的方法:
当一个变量有可能传的是对象或数组时,怎么判断是变量里传的是哪种?
注意:typeof在这个问题上不行
console.log(typeof {}); //"object"
console.log(typeof []); //"object"
区分法一(constructor):
console.log([].constructor);//function Array(){[native code]}
console.log({}.constructor);//function Object(){[native code]}
区分法二(instanceof):
console.log([] instanceof Array);//true
console.log({} instanceof Array);//false
区分法三(toString):
console.log(Object.prototype.toString.call([]));//"[object Array]"
console.log(Object.prototype.toString.call({}));//"[object Object]"
注意:这三种方法中,
instanceof和construct会有一个小问题
这个问题可能一辈子不会遇到,但确实存在
JavaScript学到后期的时候,会有父子域的问题,即一个页面里面不一定只有一个页面,可能还有一个子页面
此时,就会这样这样一个问题:
子域里面的数组instanceof父域里的array
结果却是false
js中计算精度的一个bug
在console中 输入0.14 * 100
结果是 14.000000000000002
这是一个存粹的bug,解决不了,体现了JavaScript精度不准的问题
JavaScript尽量避免小数操作,操作时要使用Math.ceil() 或Math.floor()
Math.ceil() 功能是向上取整,即(会把0.1看成1)
Math.floor() 功能是向下取整,即(把小数点后的数割掉)
再讲一个小知识点
Math.random()功能是产生一个范围在0到1之间,即开区间(0,1)的随机数
补充:计算进度会随着浏览器的升级而改变的
数组中push方法的代码实现
Array.prototype.push = function() {
for (var i = 0; i <= arguments.length; i++){
this[this.length] = arguments[i];
}
return this.length;
}
this总结
- 函数预编译过程中,this指向window
- 全局作用域中this指向window
- call/apply可以改变函数运行时的this指向
- obj.func(); //func()中的this指向obj,即函数走完预编译后,this会变化,指向调用者
那年那些题
(杂七杂八的一些题):
一:求字符串的字节长度(charCodeAt()方法可以返回指定位置的Unicode编码)
function a(target) {
var count,
len;
count = len = target.length;
for(var i = 0; i < len; i++){
if(target.charCodeAt(i) > 255)count++;
}
console.log(count);
}
二:逗号操作符的运用
var f = (function(){return "1";}, function(){return 2;})();
typeof f; //number
三:call,apply
JavaScript中call和apply方法是做什么的,有什么区别?
answer:
作用:改变this指向
区别:传参列表不同
四:函数表达式
var h = function a() {
return 123;
}
console.log(typeof a());
//result : Uncaught ReferenceError: a is not defined
//var h = function a(){...}中a在这一刻就没了
()的运用:
一:
()的运用,typeof(未定义变量)的返回值,+运算符
var x = 1;
if (function f() {}){
x += typeof f;
}
console.log(x); // "1undefined"
- 只有undefined,null,NaN,”“,false,0对应的boolean值是false,其余为true
- if(里面写的是条件),但这题特殊之处在于里面是函数声明
- 函数声明被()括起来后,同时函数声明变成了函数表达式,然后这个函数变成隐式,所以函数名f消失(即f没定义)
- 一般没定义的变量就使用会报错,但是typeof(未定义变量)会返回一个String类型的undefined
- 字符串 + 任意数据类型 => 字符串
二:
()的运用和delete
(function (x) {
delete x;
return x;
})(1);
//result:1
//形参相当于函数里面的 var x; 是不可配置属性,delete不掉
包装类:
一:
对数组的包装类的考察
题目前知识点预备:
数组本身就有属性:var arr = [1, 2, 3, 4]; arr.length = 4;
数组是可以截断的:
var arr = [1, 2, 3, 4];
arr.length = 4;
arr.length = 2;
console.log(arr); //[1, 2]
基于此,可以出题目一:
var str = "abcd";
str.length = 2;
console.log(str); //abcd
console.log(str.length); //4
数组是引用值,但是字符串是原始值
它会经历包装类,而且新建的new String(‘abcd’)与原本的str是两个东西,new String(‘abcd’)建立后又delete了,不影响原来的str
注意:length是系统自带的属性,对象字符串有这个属性,但是对象数字没有这个属性
二:加个题目:
var str = "abc";
str += 1;
var test = typeof(str);
if (test.length== 6) {
test.sign = "typeof的返回结果是string";
//new String(test).sign = 'xxx';
}
console.log(test.sign); //new String(test).sign
//undefined
this:
一:
name(形参)与 函数内的this.name 不是一个东西
function employee (name, code) {
this.name = "xiaosan";
this.code = "001";
}
var newemp = new employee("zgh", "000");
document.write("雇员姓名:" + newemp.name + "<br>"); //雇员姓名:xiaosan
document.write("雇员代号:" + newemp.code + "<br>"); //雇员代号:001
因为构造函数emplyee里面没有使用参数(name和this.name不一样哦)
二:
this与call/apply
//1
function test(){
console.log(this);
}
test.call({name : "zgh"});//Object{name : "zgh"}
//2
var name = "window";
var obj = {
name : 'zgh';
say : function(){
console.log(this.name);
}
}
obj.say.call(window);//window
//3
var name = "window";
var obj = {
name : 'zgh';
say : function(){
console.log(this.name);
}
}
var fun = obj.say;
fun();//window
//fun是全局window上的变量,所以this指向window
//3提升
var name = "window";
var obj = {
name : 'zgh';
say : function(){
console.log(this.name);
}
}
var fun = obj.say;
fun.call(obj);//zgh
三:
较为综合的一个题目
var name = "222";
var a = {
name : '111',
say : function(){
console.log(this.name);
}
}
var fun = a.say;
fun();//"222"
//a.say代表那个函数的引用,fun是全局的变量,则fun()时this指向window
a.say();//"111"
var b = {
name : "333",
say : function(fun){
fun();
}
}
b.say(a.say);//"222"
/*a.say是实参,传进形参fun中,然后fun()开始执行,
注意:不是用户调用fun(),
于是,没人调用就走预编译环节,this指向window*/
b.say = a.say;
b.say();//'333'
//这一步是把a.say拷贝到了b.say上面去,然后执行b.say(),此时this指向对象b
四:
//1
var foo = "123";
function print(){
var foo = '456';
this.foo = '789';
console.log(foo);
}
print();//'456'
//函数中的this指向window
//2
var foo = "123";
function print(){
this.foo = '789';
console.log(foo);
}
print();//'789'
//函数的调用前一刻的预编译中,this指向window
//然后第二个foo把第一个foo覆盖了
//3
var foo = "123";
function print(){
this.foo = '789';
console.log(foo);
}
new print();//'123'
//此时this已经不指向window了
闭包:
一:
实现私有化变量
function Person (name, age, sex) {
var a = 0;
this.name = name;
this.age = age;
this.sex = sex;
function sss() {
a++;
document.write(a);
}
this.say = sss;
}
var oPerson = new Person();
oPerson.say(); //1
oPerson.say(); //2
var oPerson1 = new Person();
oPerson1.say(); //1
预编译:
一:
var x = 1, y = z = 0;
function add(n) {
return n = n + 1;
}
y = add(x);
function add(n) {
return n = n + 3;
}
z = add(x);
console.log(x); //1
console.log(y); //4
console.log(z); //4
注意两个函数声明的函数名是一样的,预编译时后面的会把前面的覆盖
二:
function print(){
console.log(foo);
var foo = 2;
console.log(foo);
console.log(hello);
}
print();
执行结果:(报错)hello is not defined
这个题目看似在考察预编译,以此迷惑人,实则考察语法错误
类数组:
一:
当年阿里巴巴出了这样一道题(注意细节!):
var obj = {
"2" : 'a',
"3" : 'b',
"length" : 2,
"push" : Array.prototype.push
}
obj.push('c');
obj.push('d');
console.log(obj);//???
//答案是:Object{2:"c",3:"d",length:4}
二:
做这个放松一下
function test(){
console.log(typeof(arguments));
}
test();
//结果:object
//arguments是类数组,类数组是对象
数组
一:
随机打乱数组顺序,然后一次性返回
var arr = [1, 2, 3, 4, 5];
arr.sort(function (a, b){
return Math.random() - 0.5
});
//善于使用Math.random()
二:
把许多数组拼在一起
var str = "alibaba";
var str1 = "baidu";
var str2 = "tencent";
var str3 = "taobao";
var str4 = "wangyi";
var str5 = "yong qian chuang zao kuai le";
var arrFinal = '';
var arr = [str, str1, str2, str3, str4, str5];
for(var i = 0; i < arr.length; i++){
arrFinal += arr[i];
}
//这样写确实达到题目的要求
//但是,原始值存在栈内(有一个规则:先进后出)
//这种办法,浪费效率,不好
代码改进:
var str = "alibaba";
var str1 = "baidu";
var str2 = "tencent";
var str3 = "taobao";
var str4 = "wangyi";
var str5 = "yong qian chuang zao kuai le";
var arrFinal = '';
var arr = [str, str1, str2, str3, str4, str5];
console.log(arr.join());