@FunC
2017-11-06T22:49:11.000000Z
字数 6881
阅读 1811
JavaScript
引用类型指那些可能由多个值组成的对象。
对于引用类型的值,我们可以为其添加、删除和改变属性。但不能给基本类型的值添加属性(尽管不会报错)
复制基本类型的值时,会创建一个新值。复制引用类型的值时,其实只是复制了对象的指针。
若要检测基本类型中的string, number, boolean和undefined,typeof
操作符是最佳工具。但其它情况就不那么理想了(甚至在部分浏览器中,用typeof
检测正则表达式会返回“function”)。
比较通用的类型检测方法是使用Object.prototype.toString.call(target)
。此法在ECMASCript 5.1及以后版本中都能检测所有内置类型。
执行环境(execution context)定义了变量或函数有权访问的其它数据,决定了它们各自的行为。每个执行环境都有一个相关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中,但我们的代码无法访问这个对象,解析器在处理数据时会在后台使用它。
当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。用途是保证执行环境有权访问所有的变量和函数的有序访问。作用域链的前端,始终是当前执行代码所在执行环境的变量对象。如果该环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始只包含一个变量,即arguments
对象。
以下两种情况下能延长作用域链:
1. try-catch语句的 catch
2. with 语句
对于with语句,会将指定对象添加到作用域链中。例如:
function buildUrl() {
var qs = "?debug=true";
with(location) {
var url = href + qs; // 此处的url等价于location.href
}
return url;
}
对于catch语句,会创建一个新的变量对象,包含被抛出的错误对象的声明。
在ES6之前,ECMAScript 是没有块级作用域的,只能通过函数作用域来模拟块级作用域。
如果初始化变量时没有使用var
声明,该变量将会自动添加到全局变量中(在严格模式下则会报错)。
在读取或写入时要引用一个标识符,需经过搜索来确定标识符实际代表什么。具体的搜索方法就是沿着作用域链向上搜索,直到全局环境。
JavaScript 有自动垃圾收集机制。主要实现方式有标记清除和引用计数,现在主要使用前者。
在变量进入环境时,标记为“进入环境”。当变量离开环境时,标记为“离开环境。被标为”离开环境”的变量将在下一次垃圾收集中被回收。
尽管JavaScript 中不必操心内存管理问题,但手动解除引用仍然是优化内存占用的最佳方式。具体来说就是将其值设置为null 来释放引用。
创建Object 实例的方式有两种。
1. 使用new 操作符调用Object 构造函数
2. 使用对象字面量
其中对象字面量是向函数传递大量可选参数的首选方法。
另外访问对象属性可使用点表示法和方括号表示法,其中后者可以使用变量来访问属性。
创建Array 实例的方法同样有使用构造函数和字面对象量两种。
数组中的项能存放任意类型的数据。
数组有一个length属性,总等于数组的长度。值得注意的是,length值是可写的。也就是说,可通过改写length值来增删数组中的项。
value instanceof Array
。缺点是跨执行环境时无法检测(iframe)Array.isArray()
Object.prototype.toString.call(value)
,判断是否为”[object Array]”push 返回值:数组长度
pop 返回值:移除的项。
shift 返回值:移除的项
unshift 返回值:数组长度
sort:默认按字典顺序排序,接受排序函数,修改原数组并返回排序后的数组
reverse:修改原数组并返回排序后的数组
concat:拼接并返回新数组
slice:返回切片副本
splice:向数组中部插入项(同时可删除)
indexOf(), lastIndexOf() (使用严格相等符)
every(), filter(), forEach(), map(), some()
这些方法都会跳过值为undefined 的项
reduce(), reduceRight()
直接调用new Date() 默认获得当前时间。传入UNIX时间能得到对应的日期。传入能表示日期的字符串,内部会调用Date.parse()
,解析出对应的日期。
toDateString(), toTimeString(), toUTCString()等
ECMAScript 通过RegExp 类型来支持正则表达式。
var expression = / pattern / flags
其中flags有下面几个:
1. g: 全局模式,表示应用于所有字符串
2. i:表示不区分大小写
3. m:表示多行模式,到达一行文本末尾时还会继续查找下一行
4. y:执行“粘性”搜索,匹配从目标字符串的当前位置开始,可以使用y标志。
5. u:使用unicode模式
exex(), test()
在ECMAScript 中,函数实际上以为是对象,他们都是Function 类型的实例。
使用function
关键字声明函数时,解析器会对其进行函数声明提升(而函数表达式则不会提升)。
函数内部有arguments
和this
两个特殊的对象。前者保存着函数参数,后者是函数执行的环境对象的引用(在调用函数前,this的值不确定)。
每个函数都包含两个属性:length
和prototype
。其中length
是指函数期望接受的参数数目,而prototype
自然就是其原型,保存着各种实例方法。
每当读取一个基本类型值的时候,后台就会创建一个对应的基本包装对象类型的对象,从而让我们能调用一些方法来操作这些数据。
大致过程为:
1. 创建对应的基本包装类型实例
2. 在实例上调用指定的方法
3. 销毁这个实例
需要注意的是,number 类型在调用方法时,需要连续使用两个点操作符,来错误解析为小数:
1.toString(); // SyntaxError
1..toString(); // "1"
实质上,没有全局变量或全局函数,所有在全局作用域中定义的变量和函数,都是Global 对象的属性。
此外它还有其它的一些方法:
1. URI编码方法:encodeURI
, encodeURIComponent
2. eval方法:将字符串作为JavaScript代码执行
3. window 对象:浏览器中,Global对象以window对象的形式实现。(但除此之外还承担了很多别的任务)
提供数学中的特殊值和方法:
1. min()和max()方法
2. 舍入方法:Math.ceil(), Math.floor(), Math.round()
3. random() 方法
数据属性包含四个描述其行为的特性。(使用两对方括号包裹表示JavaScript 中不能直接访问它们)
1. [[Configurable]]: 表示是否能通过delete删除属性或修改为访问器属性。
2. [[Enumerable]]: 能否通过for-in循环返回
3. [[Writable]]: 能否修改属性值
4. [[Value]]: 这个属性的值
数据属性和访问器属性都可以使用Object.defineProperty()
来定义。
//早期方法,现在很少用
var person = new Object();
person.name = "John";
//对象字面量语法,直观
var person = {
name: "John"
}
存在问题:当需要创建多个对象时,要写大量重复代码
1.工厂模式
将创建对象的过程封装成一个函数
function createPerson(name){
var o = New Object();
o.name = name
return o;
}
var person1 = createPerson("John");
var person2 = createPerson("Mike");
工厂模式的问题:创建的对象没有其类型的名字,所以无法判断两个对象是否为同一个类型
2.构造函数模式
为了解决对象类型的问题,可使用构造函数来创建对象,该方法的核心在于使用了new
操作符
来看看对函数使用new
操作符会发生什么
- 创建一个新的对象
- 将构造函数的作用域赋给新对象(因此
this
指向了这个新对象)- 执行构造函数中的代码
- 返回创建的新对象
于是我们就可以把构造函数写成如下形式:
function Person(name){
this.name = name;
this.sayName = function(){
alert(this.name);
}
}
var person1 = new Person("John");
var person2 = new Person("Mike");
注意其实不用new
操作符这个函数也能跑,只是this
指针就指向了windows
对象了;为了表明这是我们定义的一种对象类型,函数名的首字母要大写(毕竟关键字都是function
,容易和一般的函数混淆)
这样,person1和person2分别保存着Person的一个不同的实例,而且两个对象都有一个constructor属性,该属性指向构造函数Person,如下所示:
alert(person1.constructor == Person);//true
alert(person2.constructor == Person);//true
直接看例子
console.log(person1.sayName == person2.sayName);//false
可以看到,用上述方法定义的方法是两个不同的对象实例
this.sayName = New Function(console.log(this.name));//这种写法等价于上述写法
然而这个方法的功能是一样的,创建不同的实例是没有必要的。所以调用方法时,其实只要找到对应的函数即可,而不是调用时创建一个函数。那么我们其实可以把函数写在构造函数外面:
function Person(name){
//...
this.sayName = sayName;
}
function sayName(){
console.log(this.Name);
}
新的问题来了:这些定义在全局作用域的函数只能被某些对象使用,似乎失去了全局作用域的意义。另一方面,各种构造函数的方法都混杂着定义在全局当中,对构造函数来说毫无封装性可言。
3.原型模式
每一个函数都有prototype属性,属性值是指向其原型对象的指针(对应的原型对象有constructor属性指向构造函数)。该原型对象的属性和方法由所有实例共享。
所以推荐把方法定义在原型上
注意:如使用对象字面量改写原型:
Person.prototype = {
this.name = name;
}
实质上变更了Person.prototype指针的指向,而不是在原来的基础上修改,因此也丢失了constructpr属性。
4.组合使用构造函数模式和原型模式(推荐)
综上,创建自定义类型时,建议方法定义在原型上,属性定义在构造函数当中。
加强版(定义在原型上的方法也写进构造函数中):
function Person(name, age, job){
//属性
this.name = name;
this.age = age;
this.job = job;
//方法
//含多个方法时也只需要一条if语句,因为能通过表示初始化成功
if( type of this.sayName != 'function'){
Person.prototype.sayName = function(){
alert(this.name);
}
}
}
5.寄生构造函数模式(其实是工厂模式,但可以用new
)
试想我们要构建一个具有特殊功能的数组对象,要怎么做?
不建议修改原生对象的原型。在不同环境下可能发生命名冲突;还有可能意外重写原生方法。
function specialArray(){
//创建数组
var values=new Array();
//添加值
//注意这里用apply是为了把arguments这个伪数组拆开,否则等于把arguments这个对象整体作为values数组的一项
//ES6中可以用拓展运算符代替,更直观,即values.push(...arguments);
values.push.apply(values,arguments);
//添加方法
values.toPipedstring=function(){
return this.join("|");
}
//返回数组
return values;
}
var colors=new specialArray('red','yellow','green');
alert(colors.toPipedstring()); // red|yellow|green
其实就是可用new的工厂模式,工厂模式的缺点它都有,用instanceof运算符也只能返回Object,尽量不建议使用这种模式;
6.稳妥构建函数模式(安全)
稳妥对象:即无公共属性,同时其方法不引用this的对象。
即传入参数直接用于构建方法,不在属性上停留。如:
function Person(name){
var o = new Object();
0.sayName = function(){
alert(name);
};
return o;
}
试想一下,如果用原型A的实例来作为构造函数B的原型会发生什么?
没错,就是实例B拥有了原型A的属性和方法。
因为这涉及到指针操作,所以要注意赋值操作的顺序
1.结构构造函数(相当于构造函数代码拼接)
直接看代码吧:
function SuperType(name){
this.name = name;
}
function SubType(){
//把this传递过去相当于把上面的代码复制了过来
SuperType.call(this, "Nicholas");
//实例属性
this.age = 29;
}
还是构造函数的老问题,方法不共用
2.组合继承(ES5最常用)
即属性用构造函数继承,方法通过原型链继承
//超类型方法的定义
SuperType.prototype.sayName = function(){
alert(this.name);
}
//子类型继承属性
function subType(name, age){
SuperType.call(this, name);
this.age = age;
}
//子类型继承方法
SubType.prototype = new SuperType();
//子类型定义方法
SubType.prototype.sayAge = alert(this.age);
3.原型式继承(即对象的浅复制)
思想是用一个实例函数作为另一个对象的原型。
ES5中提供了方法Object.create()
来实现,第一个参数为用作原型的实例对象,第二个参数(可忽略)为额外的属性值,需要同时写上属性描述符;
4.寄生式继承
即用函数把定义自己的方法这一操作打包起来,同样方法不能共享。
5.寄生组合式继承(最优选择)
细心的话可以发现,上述的组合继承中在原型中继承的属性是多余的
实现方法如下:
function inheritPrototype(subType, SuperType){
//浅复制超类型的原型(即只含有方法)
var prototype = Object.create(SuperType.prototype);
//将刚才浅复制的原型的constructor指向subType
prototype.constructor = subType;
//subType的prototype指向上面复制的原型
subType.prototype = prototype;
}