0%

对象

6.1 对象

6.11 对象创建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//对象
//对象的创建

var obj1=[];
var obj2=new Object();
//两个方法都可以
/*注意:与 function 不同的是,对象的变量要用逗号间隔开,最后一个变量后面不用加符号*/
var objBook = {
name:'Book Name',//原始成员
pageNumber:300,
author:{//引用成员
firstname:'aaa',
lastname:'bbb'
},
query:function(){//成员函数
console.log('query');
},
addPage:function(){
this.pageNumber++;
console.log('addPage');
}
};

console.log(++objBook.pageNumber);//301
objBook.query();//query
objBook['query']();//query

6.12 对象属性

在 js 中,对象要怎么增加属性呢,有以下这几种方法:

1
2
3
var obj={};
obj.a='aaa';
obj['b']='bbb';//等价于 obj.b='bbb';

object 增加属性不需要声明,直接使用就可以,例如:

1
2
3
obj.f1=function(){
console.log('fff......');
}

创建了对象属性之后,想要删除修改要怎么做呢?删除属性,首先是先查询一个属性是否存在。例如:

1
2
3
4
//1.in
console.log(('a' in obj));//true
//2.hasOwnProperty()
console.log(obj.hasOwnProperty('a'));//true

当结果为 true 时,表示系统查询到了该属性,接下去就是删除,删除一般就一个方法,delete :

1
2
3
4
5
6
7
8
//delete
delete obj.a;
console.log(('a' in obj));//false,表示已经删除
console.log(obj.hasOwnProperty('a'));//false
//
var arr=[];
delete arr[0];
console.log(arr);

这是属性的删除方法。

那改的话怎么改呢,修改属性其实很简单的,直接上手改动就行:

1
obj.a=123;

在这里,a 已经被改掉了值。然后呢,有一个东西叫做枚举,看到这,是不是大家就会感觉到其实 object 跟数组极其相似。枚举是什么呢?

1
2
3
4
for(var p in obj){
console.log(p);//1 a f1
}
obj.toString();//1 a f1

我们调用 toString 的时候,结果也是跟 for 循环的结果一样,所以说,toString 并不是直接挂在 obj 上面的,它是挂在它底下的原形上的,这是一种类似继承的方法。 javascript 所面对的环境并不是很复杂的环境,主要是脚本前端的渲染,交互和一些游戏这些方面的语言,给人一眼看过去就会的语言。能够枚举的东西都是它自己的,不能枚举的都是它从别人那继承过来的,而且这种继承关系更像是一种委托。就类似于设计界找枪手的例子,为了自己的某种目的,把别人的东西变成自己的。怎么检测一个东西是否可以枚举呢?

1
2
console.log(obj.propertyIsEnumerable('a'));//true
console.log(obj.propertyIsEnumerable('toString()'));//false

6.13 自定义构造函数

我们现在的对象都是从 object 里面生成出来的,在上面加上我们想要添加的东西,那我们是不是每做一个对象,都要重复这样操作呢,其实不是的,有一个东西叫做构造函数。返回对象创建那块,如果我们需要两个对象的情况下,怎么做,直接 copy ?当然是 no 了,这样修改其中一个对象的话也会麻烦,更加不符合我们之前说的同样的事情要用同样的过程把它分装起来,类似的步骤,我们可以把它分装在固定的位置,好处就是以后在不同的地方调用,改一个函数即可。

所以,我们需要一种函数能够每次以相同的方式构造对象,同时,在改动这个函数所有的对象都能跟着改变,这就需要一个构造函数,说的通俗点就是 class 。构造函数名称以大写字母开头,这只是约定俗成,并不是语言要求。举例说下,

1
2
3
4
5
6
function Student(name,age,gender){
this.name=name;
this.age=age;
this.gender=gender;
}
var std=new Student('WangLin',30,'male');

在这边呢,有三种命名规则:

  • 匈牙利命名规则:属性 + 类型 + 对象描述

    属性:成员变量 m_ 静态成员 s_ 普通 <没有>

    类型:整形 i ,数组 a ,字符串 str

    对象描述:单词 + 单词。。。。,首字母大写

  • 小驼峰命名规则:对象描述

    第一个单词的首字母小写,其他单词的首字母大写

  • 大驼峰命名规则:对象描述

    第一个单词的首字母大写,其他单词的首字母大写

    命名是很重要的,体现了我们代码人的素养。越后来,可能越来越少的人会使用匈牙利命名规则,特别是 window 不是特别热的情况下,所以,小驼峰和大驼峰会成为大多数人的趋向。我们会发现构造函数使用的是大驼峰命名规则,而 object 对象的成员函数使用的是小驼峰命名规则。

6.14 包装类

  • Number() vs number

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var n1=123;
    var n2=new Number(123);//构造函数

    console.log(typeof(n1));//number
    console.log(typeof(n2));//object

    var n3=n1+n2;
    console.log(n3);//246

  • String() vs string

    1
    2
    3
    4
    5
    var s1='aaa';
    var s2=new String('aaa');
    console.log(typeof(s1));//string
    console.log(typeof(s2));//object

  • Boolean() vs boolean

    1
    2
    3
    4
    5
    var b1=true;
    var b2=new Boolean(true);
    console.log(typeof(b1));//boolean
    console.log(typeof(b2));//object

  • 包装类的隐式调用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var arr=[1,2,3,4];
    arr.length=2;
    console.log(arr);//[1,2]

    var str='1234';
    //str.length=2;
    var strTmp=new String(str);
    strTmp.length=2;
    //strTmp 摧毁
    console.log(str);//1234

    var iNum=123;
    //iNum.toString();
    var iTmp=new Number(iNum);
    iTmp.toString();
    //iTmp 摧毁
    console.log(iNum);//123

6.2 预编译

JavaScript 引擎的三大步骤:

  • 预编译(第一次\前置扫描)

    脚本:

    • 创建全局对象 GO(window) (上下文)

    • 加载脚本文件

    • 预编译

      • 找出所有的变量声明,按照变量名加入全局对象,如果已经存在,忽略。
      • 找出所有的函数声明,按照函数名加入全局对象,如果已经存在同名变量或者函数,替换。
      • 非声明不予理睬
    • 解释执行

  • 解释执行(第二次扫描)

6.21 预编译——脚本

编译呢,是指把代码变成块结构的语言,就是机器码,然后把编好的块链接起来,组装成 .exe 或者是 .dl 的过程叫做链接。预编译其实是编译里最早的东西。在 c++ 里面,我们拿到一个函数都是先把它声明,再去实现,那么,预编译是把所有的声明都扫描出来,扫描到一个名字表里面去,然后知道这个东西以后,在编译时才能知道是否有这么一个东西。

没有 var 的变量(都不是变量声明),全部认为是 window 的全局变量,不参与预编译。举个简单的例子:

1
2
aa=5;
console.log(aa);//5

这个结果是毫无疑问的,那么,我们再来看个,

1
2
3
4
5
console.log(aa);
aa=5;
console.log(aa);
//Uncaught ReferenceError: aa is not defined
at ds7-5.js:4

结果是会报错,那如果按照预编译的理解,改成这样子呢,

1
2
3
console.log(aa);//undefied
var aa=5;
console.log(aa);//5

为什么会这样子?很简单,就是我们上面说的,这是段脚本语言,拿到这个脚本,先是创建全局对象,然后按照流程,找出所有的变量声明,这边只有 aa ,把变量名 aa 加入全局对象,即aa 已经存入 window 中,没有函数声明,就直接继续执行,执行的话,第一行语句,因为 aa 已经存进 window 中,但是没有定义,所以输出的结果是 undefined ,然后第二行语句,将5赋值给 aa ,所以第三行语句输出5。

即使在函数中,aa 也是全局变量,是在运行时,而不是定义时。举个例子:

1
2
3
4
function test(){
a=5;
}
console.log(a);

大家根据预编译的步骤想想,这样会不会报错,会的,因为 test() 函数并没有被纳入 window 中,想让程序不报错的话,我们要先定义函数,让它首先被 window 存入,例如:

1
2
3
4
5
test();
function test(){
a=5;
}
console.log(a);//5

这样子结果就是正确的。

在脚本中,所有变量声明在脚本的预编译阶段完成,所有变量的声明与实际的书写位置无关。例如:

1
2
3
console.log(aa);
var aa=5;
console.log(aa);

脚本中,所有函数声明在脚本的与编译阶段完成,所有函数的声明与实际书写位置无关。例如,

1
2
3
4
5
6
7
8
console.log(haha);
function haha(){
console.log('h1');
}
//结果:
//ƒ haha(){
// console.log('h1');
//}

脚本中,如果变量与函数同名,那么,函数将覆盖变量。举例:

1
2
3
4
5
6
7
8
9
console.log(haha);
var haha=123;
function haha(){
console.log('h1');
}
//结果:
//ƒ haha(){
// console.log('h1');
//}

在这里,函数就将变量给覆盖掉,不然的话,运行结果应该在本结果的前面加一个 undefined 。

脚本中,只有函数能够覆盖变量,变量无法覆盖函数。例如:

1
2
3
4
5
console.log(haha);
function haha(){
console.log('h1');
}
var haha=123;

脚本中,后面的函数声明会覆盖前面的函数声明,并且会忽略参数。例如:

1
2
3
4
5
6
7
haha(1);
function haha(a){
console.log('haha1');
}
function haha(a,b){
console.log('haha2');
}

6.22 预编译——函数调用

函数调用:

  • 创建活动对象 AO(Active Object)

  • 预编译:

    • scope chain
    • 初始化 arguments
    • 初始化形参,将 arguments 中的值赋给形参
    • 找出所有的变量声明,按照变量名加入 AO ,如果已经存在,忽略。
    • 找出所有的函数声明,按照函数名加入 AO ,如果已经存在同名变量或者函数,替换。
    • this 初始化
  • 解释执行

函数中,所有变量声明,在函数的预编译阶段完成,所有变量的声明与实际书写位置无关。例如:

1
2
3
4
5
6
function f(){
console.log(aa);
var aa=5;
console.log(aa);
}
f();

函数中,所有函数声明在函数的预编译段完成,所有函数的声明与实际书写位置无关。例如:

1
2
3
4
5
6
7
function f(){
console.log(haha);
function haha(){
console.log('h1');
}
}
f();

函数中,如果变量与函数同名,那么,函数将覆盖变量。举例,

1
2
3
4
5
6
7
8
function f(){
console.log(haha);
var haha=123;
function haha(){
console.log('h1');
}
}
f();

函数中,只有函数能够覆盖变量,变量无法覆盖函数。例如:

1
2
3
4
5
6
7
8
9
function f(){
console.log(haha);

function haha(){
console.log('h1');
}
var haha=123;
}
f();

函数中,后面的函数声明会覆盖前面的函数声明,并且会忽略参数。例如:

1
2
3
4
5
6
7
8
9
10
11
function f(){
console.log(haha);

function haha(a){
console.log('haha1');
}
function haha(a,b){
console.log('haha2');
}
}
f();

当函数预编译后,遇到需要访问的变量或者函数,优先考虑自己 AO 中定义的变量和函数,如果找不到才会在其定义的上一层 AO 中寻找
,如果再找不到才会在其定义的上一层 AO 中寻找,直到到达 GO 。例如:

1
2
3
4
5
6
7
8
var scope='global';
function t(){
console.log(scope);//undefined
var scope='local';
console.log(scope);//local
}
t();
console.log(scope);//global

首先是脚本的预编译,变量 scope , t 已经放入 window 中, 大致思路如下:

1
2
3
4
5
6
GO:
scope:undefined ->'global'
t:function

t-AO://函数执行退出的时候, t-AO 被摧毁
scope:undefined ->'local'

看个前端工程师的面试题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log(scope);//undefined
var scope='global';
function t(){
var scope='t-local';
function t2(){
console.log(scope);//undefined
var scope='t2-local';
console.log(scope);//t2-local
}
t2();
console.log(scope);//t-local
}
t();
console.log(scope);//global

遵循函数调用的原则,大致思路如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//GO:
//scope:undefined
//t:function//预编译结束
//
////运行
//GO:
//scope:undefined ->global
//t:function
//
////预编译
//t-AO:
//scope:undefined
//t2:function
//
////运行
//t-AO:
//scope:undefined ->'t-local'
//t2:function
//
//t2-AO:
//scope:undefined ->'t2-local'//被摧毁
您的支持是对我最大的鼓励