JavaScript之ECMAScript笔记(基础)

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的东西,这没什么好区分的

变量

  1. 变量名必须以 英文字母_$ 开头
  2. 变量名可以包括英文字母_$数字
  3. 不可以用系统的关键字保留字作为变量名
  4. 区分大小写
  5. 类型不需要定义,由值决定类型

数据类型概要:

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不等于任何数,包括它自己)

避免发生隐式类型转换 :
绝对等于 ===
绝对不等与 !==


基本语法

  1. 语句后面要用分号结束“”(英文字符的分号)
  2. 书写格式要规范,运算符两边都应该有空格

编译错误

一个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 暗示全局变量:即任何变量,如果未经声明就赋值,此变量就为全局对象所有
  • 全局对象是windowa = 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

要求:

  1. typeof([]) – array
  2. typeof({}) – object
  3. typeof(function) – object
  4. typeof(new Number()) – number Object
  5. typeof(123) – number

思路:

  1. 分两类:原始值和引用值
  2. 区分引用值
  3. 注意: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}

/*加lengthpush方法后,这个对象可以
调用数组的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());

猜你喜欢

转载自blog.csdn.net/Zhangguohao666/article/details/81634496