JavaScript高级编程小结
Undefined
对未初始化的变量执行typeof操作符会返回undefined
值,而对未声明的变量执行typeof操作符同样也会返回undefined
var message;
console.log(typeof message); // => undefined
console.log(typeof gaga); // => undefined
Boolean
各种类型转换成Boolean的规则
数据类型 | 转成true的值 | 转成false的值 |
---|---|---|
Boolean | true | false |
String | 任何非空字符串 | ""空字符串 |
Number | 任何非零数字值(包括无穷大) | 0和NaN |
Object | 任何对象 | null |
Undefined | n/a | undefined |
Number
Number类型应该是ECMAScript中最令人关注的数据类型了。
除了以十进制表示外,整数还可以通过八进制或十六进制表示,其中,八进制字面值的第一位必须是0,然后是八进制数字序列(0 ~ 7)。如果字面值中的数值超出了范围,那么前导0将被忽略,后面的数值将被当作十进制数值解析
var n = 070; // => 56
var n = 079; // => 79(无效的八进制数值)
var n = 08; // => 8(无效的八进制数值)
八进制字面量在严格模式下是无效的,会导致支持的JavaScript引擎抛出错位。
十六进制字面值的前两位必须是0x,后边跟着任何十六进制数字(0 ~ 9 及 A ~ F)。其中,字母A ~ F 可以大写,也可以小写。
var n = 0xA; // 10
var n = 0x1f; // 31
计算的时候,八进制和十六进制都将转成十进制后再计算。
由于保存浮点数值需要的内存空间是保存整数值的两倍,因此ECMAScript会不失时机的将浮点数值转换为整数值。
永远不要测试某个特定的浮点数值:
if (a + b == 0.3) {
alert("You got 0.3");
}
上边的例子中,我们测试的是两个数的和是不是等于0.3。如果这两个数是0.05和0.25,或者是0.15和0.15都不会有问题。如果这两个数是0.1和0.2,那么测试就无法通过。
由于内存的限制,ECMAScript并不能保存世界上所有的数值。如果某次计算的结果得到了一个超出JavaScript数值范围的值,那么这个数值将被自动转换成特殊的Infinity值,如果这个数值是负数,则会转成-Infinity。出现正或负的Infinity值就不能继续计算了。可以使用isFinite()函数判断一个数值是不是有穷的。
NaN是Not a Number的缩写,它有两个非同寻常的特点:
- 任何涉及NaN的操作都会返回NaN
- NaN与任何值都不相等,包括NaN本身
isNan()函数的原理是:在接受一个值后,会尝试将这个值转换成数值,成功就返回false,失败则返回true。
有3个函数可以把非数值转换成数值:Number(),parseInt()和parseFloat()。Number函数可以用于任何数据类型,另外两个则专门用于把字符串转换成数值。
Number()函数的转换规则如下:
-
如果是Boolean值,true和false将分别被转换为1和0
-
如果是数字值,只是简单的传入和返回
-
如果是null值,返回0
-
如果是undefined,返回NaN
-
如果是字符串,遵循下列规则:
- 如果字符串中只包含数字(包括前面带正好或负号的情况),则将其转换为十进制数值,即“1”变成1,“123”会变成123,而“011”会变成11(注意:前导的0被忽略了)
- 如果字符串中包含有效的浮点格式,如“1.1”,则将其转换为对应的浮点数值(同样会忽略前导0)
- 如果字符串中包含有效的十六进制格式,例如“0xf”,则将其转换为相同大小的十进制整数值
- 如果字符串是空的(不包含任何字符),则将其转换为0
- 如果字符串中包含除上述格式之外的字符,则将其转换为NaN
-
如果是对象,则调用对象的valueOf()方法,然后依照前面的规则转换返回的值,如果转换的结果是NaN,则调用对象的toString()方法,然后再一次按照前面的规则转换返回的字符串值。
var n = Number("Hello world"); // NaN
var n = Number(""); // 0
var n = Number("000011"); // 11
var n = Number("true"); // 1
parseInt()和parseFloat()在使用的时候需要特别注意进制的问题,parseFloat()只解析十进制。
String
String()方法内部转换规则:
- 如果值有toString()方法,则调用该方法并返回相应的结果,toString()方法不能处理null和undefined的情况
- 如果值是null,则返回“null”
- 如果值是undefined,则返回“undefined”
var n1 = 10;
var n2 = true;
var n3 = null;
var n4;
console.log(String(n1)); // => "10"
console.log(String(n2)); // => "true"
console.log(String(n3)); // => "null"
console.log(String(n4)); // => "undefined"
逻辑与
逻辑与(&&)可以应用于任何类型的操作数,而不仅仅是布尔值。在有一个操作数不是布尔值的情况下,逻辑与操作就不一定返回布尔值,它遵循下列规则:
- 如果第一个操作数是对象,则返回第二个操作数
- 如果第二个操作数是对象,则只有在第一个操作数的求值结果为true的情况下才会返回该对象
- 如果两个擦作数都是对象,则返回第二个操作数
- 如果有一个操作数是null,则返回null
- 如果有一个操作数是NaN,则返回NaN
- 如果有一个操作数是undefined,则返回undefined
逻辑与操作属于短路操作,即如果第一个操作数能够决定结果,那么就不会再对第二个操作数求值,这个跟有些语言不一样,因此在条件语句中使用逻辑与的时候要特别注意。
var n = true && NaN;
console.log(String(n)); // => NaN
var n2 = Boolean(n);
console.log(n2); // => false
if (!n) {
console.log("ok"); // => ok
}
打印出了ok,说明在条件语句中可以使用&&,但是需要明白返回值的问题。
相等操作符
相等(==)操作符在进行比较之前会对操作数进行转换,我们要了解这个转换规则:
- 如果有一个操作数是布尔值,则在比较相等性之前先将其转换为数值,false转换为0,而true转换为1
- 如果一个操作数是字符串,另一个操作数是数值,在比较相等性之前先将字符串转换为数值
- 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf()方法,用得到的基本类型值按照前面的规则进行比较
- null和undefined是相等的
- 要比较相等性之前,不能将null和undefined转换成其他任何值
- 如果有一个操作数是NaN,则相等操作符返回false,而不相等操作符返回true。重要提示:即使两个操作数都是NaN,相等操作符也返回false
- 如果两个操作数都是对象,则比较他们是不是同一个对象。如果两个操作数都指向同一对象,则相等操作符返回true,否则,返回false
全等(=)和相等()最大的不同之处是它不会对操作数进行强制转换。
参数传递
ECMAScript中所有函数的参数都是按值传递的。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。有不少开发者在这一点上可能会感到困惑,因为访问变量有按值和按引用两种方式,而参数只能按值传递。
在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者用ECMAScript的概念来说,就是arguments对象中的一个元素)。在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。
先看一个基本类型值传递的例子:
function addTen(num) {
num += 10;
return num;
}
var count = 10;
var result = addTen(count);
console.log(count); // => 10
console.log(result); // => 20
上边的代码中,addTen函数并没有改变count的值,按照上边的理论,我们可以这么看addTen函数:
function addTen(num) {
num = count; // 当调用了函数的时候,函数内部做了这一个操作
num += 10;
return num;
}
再来看看引用类型的值传递的例子:
function setName(obj) {
obj = person; // 当调用了函数的时候,函数内部做了这一个操作
obj.name = "James";
obj = new Object();
obj.name = "Bond";
}
var person = new Object();
setName(person);
console.log(person.name); // => "James"
在函数内部,同样为参数赋值了一个引用类型值的复制数据。在函数内部,obj就是一个指针,当给他重新赋值一个新的对象的时候,他指向了另一个数据,因此,即使给它的name赋值,也不会影响函数外部的对象的值,说白了,还是内存地址的问题。
Array
数组的length
属性很有特点------他不是只读的。因此通过设置这个属性,可以从数组的末尾移除项或向数组中添加新项:
var colors = ["red", "blue", "green"];
colors.length = 2;
alert(colors[2]); // => undefined
colors.length = 4;
上边的代码给colors设置了length后,最后边的那个数据就变成了undefined,说明通过设置length能够修改数组的值,如果这个值大于数组元素的个数,那么多出来的元素就赋值为undefined。
数组的sort()方法会调用每个数组项的toString()转型防范,然后比较得到的字符串,以确定如何排序。即使数组中的每一项都是数值,sort()方法比较的也是字符串。 看个例子:
var array = [1, 4, 5, 10, 15];
array = array.sort();
console.log(array.toString()); // => 1,10,15,4,5
可见,即使例子中值的顺序没有问题,但sort()方法也会根据测试字符串的结果改变原来的顺序。
数组有5种迭代方法:
- every(): 对数组中的每一项运行给定函数,如果该函数对每一项都返回true,则返回true,就跟它的名字一样,测试数组中是否每一项都符合函数的条件
- some(): 对数组中的每一项运行给定函数,如果该函数对任一项返回true,则返回true,同样,就跟它的名字一样,测试数组中是否存在至少一项是符合函数的条件
- filter(): 对数组中的每一项运行给定的函数,返回该函数会返回true的项组成的数组, 主要用于过滤数据
- forEach(): 对数组中华的每一项运行给定函数,这个方法没有返回值,就是遍历方法
- map(): 对数组中的每一项运行给定函数,返回每次函数调用的结果组成的数组,这个算是对数组中的项进行加工
var numbers = ["1", "2", "3", "4", "5", "6"];
// every() 检测数组中的每一项是否都大于2
var everyResult = numbers.every(function (item, index, array) {
return item > 2;
});
console.log(everyResult); // => false
// some() 检测数组中是否至少有一项大于2
var someResult = numbers.some(function (item, index, array) {
return item > 2;
});
console.log(someResult); // => true
// filter() 过滤数组中大于2的值
var filterResult = numbers.filter(function (item, index, array) {
return item > 2;
});
console.log(filterResult); // => ["3", "4", "5", "6"]
// map() 加工数组中的数据
var maoResult = numbers.map(function (item, index, array) {
return item * 2;
});
console.log(maoResult); // => [2, 4, 6, 8, 10, 12]
Function
使用函数作为返回值是一件很奇妙的事情,我们使用一个例子来看看:
function createComparisonFunction(propertyName) {
return function (object1, object2) {
var value1 = object1[propertyName];
var value2 = object2[propertyName];
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
}
var data = [{
name: "zhangsan",
age: 20
}, {
name: "lisi",
age: 30
}];
data.sort(createComparisonFunction("name"));
console.log(data[0]); // => {name: "lisi", age: 30}
data.sort(createComparisonFunction("age"));
console.log(data[0]); // => {name: "zhangsan", age: 20}
在函数内部,有两个特殊的对象:arguments和this。其中,arguments是一个类数组对象,包含着传入函数中的所有参数。虽然arguments的主要用途是保存函数参数,**但这个对象还有一个名叫callee的属性,该属性是一个指针,指向拥有这个arguments对象的函数,我们看下边这个非常经典的阶乘函数:
function factorial(num) {
if (num < 1) {
return 1;
} else {
return num * factorial(num - 1);
}
}
console.log(factorial(5)); // => 120
定义阶乘函数一般都要用到递归算法,如上边的代码所示,在函数有名字,而且名字以后都不会变的的情况下,这样定义没问题。但问题是这个函数的执行与函数名factorial仅仅耦合在了一起。为了消除这种紧密耦合的现象,可以像下面这样是哟很难过arguments.callee:
function factorial(num) {
if (num < 1) {
return 1;
} else {
return num * arguments.callee(num - 1);
}
}
console.log(factorial(5)); // => 120
我们修改factorial函数的实现后:
const anotherFactorial = factorial;
factorial = function () {
return 0;
}
console.log(anotherFactorial(5)); // => 120
console.log(factorial(5)); // => 0
使用call()或apply()来扩充作用域的最大好处,就是对象不需要与方法有任何耦合关系。
属性类型
ECMAScript中有两种属性:数据属性和访问器属性。
1.数据属性
数据属性包含一个数据值的位置。在这个位置可以读取和写入值。数据属性有4个描述其行为的特性:
- configurable
- enumerable
- writable
- value
我们先看几个例子:
const person = { };
Object.defineProperty(person, "name", {
writable: false,
value: "James"
});
console.log(person.name); // => James
person.name = "Bond";
console.log(person.name); // => James
上边的代码设置了person中的属性name的特性,把它的writable设置为false,因此当我们重写它的name属性的时候是不起作用的,使用value可以给属性赋值。我们再看一个例子:
const person = { };
Object.defineProperty(person, "name", {
configurable: false,
value: "James"
});
console.log(person.name); // => James
delete person.name;
console.log(person.name); // => James
当我们把confugurable设置为false的时候,就把name属性的可配置性给锁死了,一旦把confugurable设为false,后续的再次对这个属性设置特性的时候就会出错。下边的代码会报错:
Object.defineProperty(person, "name", {
writable: true,
value: "JJJJJ"
});
console.log(person.name);
2.访问器属性
访问器属性不含数据值,但可以通过set或get方法来设置或获取值,就像制定了一套这样的规则。我跟喜欢称这个特性为计算属性。
const book = {
_year: 2004,
edition: 1
};
Object.defineProperty(book, "year", {
get: function () {
return this._year;
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005;
console.log(book.edition);
在这个例子中。_year很想一个私有变量,我们通过set,get方法来写了一个year属性,当然也可以使用这种方式来控制属性是否只读或只写特性。
有一点值得注意,上边说的这些内容算是为对象创建属性的方法,我们也可以采用person.name这种方式创建属性,只不过后边这种创建的方式给里边的特性赋了默认的值。
创建对象
在JavaScript中Object的总结这篇文章中,我介绍了多种创建对象的方法:
工厂方法
核心思想是通过函数来创建对象,函数会返回一个根据参数创建的新的对象,这个方法虽然解决了创建多个相似对象的问题,但没有解决对象识别的问题,因为在函数内容,知识把参数赋值给了任何对象的属性
构造函数
构造函数的使用方法我就不提了,我只说几点需要注意的地方,构造函数的第一个字母要大写,内部使用this来指定属性和方法。在创建对象的时候要加上new关键字。
其实构造函数的本质也是一个函数,如果在调用的时候不加关键字new,那么它内部的属性将会创建为全局变量的属性。**任何加上new关键字的函数都会变成构造函数,而构造函数的本质是:
var a = {};
a.__proto__ = F.prototype;
F.call(a);
构造函数能够让我们通过类似.constructor或instanceof来判断对象的类型,但它的缺点是会为相同的属性或方法创建重复的值,我们都知道在JavaScript中函数也是对象,这种返回创建统一对象的过程,肯定给性能带来了很大的挑战,因此这种模式还需要升级。
原型模式
原型模式是非常重要的一个概念,我们会使用很长的篇幅来介绍这方面的内容。
首先我们应该明白函数名字本质上是一个指向函数对象的指针,因此他能表示这个函数对象,在JavaScript中每个函数**内部都有一个prototype属性,这个属性是一个指针,指向一个对象,而这个对象的用于是包含属性和方法。因此我们有这样的启发,如果我给构造函数的prototype赋值属性和方法,那么我在使用构造函数创建对象的时候,是不是就可以继承这些共有的属性呢? 答案是肯定的:
function Person() {
}
Person.prototype.name = "James";
Person.prototype.sayName = function () {
console.log(this.name);
};
const person1 = new Person();
person1.sayName(); // => James
const person2 = new Person();
person2.sayName(); // => James
console.log(person1.name == person2.name); // => true
1.理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性。这个属性指向函数的原型西乡,在默认情况下,这个prototype又会自动获取一个叫做constructor的属性,这个属性包含一个指向prototype属性所在函数的指针,可以说这是一个回路。
那么创建一个实例的过程是怎么样的呢?
当我们用构造函数创建一个实例后,该实例内部也会有一个指针指向构造函数的原型对象,一般情况下,这个指针的名字并不是prototype,我们必须记住一点,prototype只是函数内部的一个属性。大部分浏览器的这个指针是__proto__
。我们看一张图:
上图很好的展示了构造函数和实例对象之间原型的关系。我们在这里就不一一说明了。虽然我们通过__proto__
能访问到原型对象,但这绝对不是推荐做法。我们可以通过isPrototypeOf()
方法来确定对象之间是否存在这种关系:
console.log(Person.prototype.isPrototypeOf(person1)); // => true
上边的代码很好的演示了这一说法,实例对象person1的原型就是构造函数Person的prototype。,还有一个方法是获取原型对象getPrototypeOf()
:
console.log(Object.getPrototypeOf(person1) == Person.prototype); // => true
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性,如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性,如果在原型对象中找到这个属性,则返回该属性。具体的例子我们就不演示了。
值得注意的是,当给对象的属性赋值时,如果属性的名称与原型对象的属性名称相同,对象内部会创建这个属性,原型中的属性保持不变。,我们可以这么认为,原型对象大部分时候只提供读取功能,它的目的是共享数据。但如果给引用类型的属性赋值的时候会有不同的情况,比如修改原型的对象,数组就会导致原型的数据遭到修改。这个在JavaScript中Object的总结这篇文章中我已经详细的给出了解释。
2.原型与in操作符
通过上边的距离,我们大概明白了对象属性与原型之间的关系,那么现在就引出了一个问题。如何区分某个属性是来自对象本身还是原型呢?为了解决这个问题,我们引出in
操作符。
有两种方式使用in操作符:单独使用和在for-in循环中使用。在单独使用时,in操作符会在通过对象能够访问给定属性时返回true,无论该属性存在于实例中还是原型中。我们看下边这个例子:
function Person() {
}
Person.prototype.name = "James";
Person.prototype.sayName = function () {
console.log(this.name);
};
const person1 = new Person();
console.log(person1.hasOwnProperty("name")); // => false
console.log("name" in person1); // => true
hasOwnProperty()
方法能够判断对象本身是否存在某个属性,而in能够判断对象是否能够访问某个属性,结合这两种方法,我们就能判断某个属性的来源,我们举个简单的例子:
function hasPrototypeProperty(object, name) {
return (!object.hasOwnProperty(name)) && (name in object);
}
console.log(hasPrototypeProperty(person1, "name")); // => true
for-in可以遍历对象中的属性,**但是要依赖属性中的enumerable
这个特性的值,如果这个值为false,那么就无法遍历到属性,跟for-in很相似的方式是Object.keys()
他返回一个字符串数组,如果要想遍历出对象的属性,忽略enumerable
的影响,可以使用Object.getOwnPropertyNames()
这个方法,下边是一个简单的例子:
function Person() {
}
Person.prototype.name = "James";
Person.prototype.sayName = function () {
console.log(this.name);
};
const person1 = new Person();
console.log(hasPrototypeProperty(person1, "name")); // => true
Object.defineProperty(person1, "age", {
enumerable: false
});
for (const pro in person1) {
console.log(pro);
}
const keys = Object.keys(person1);
console.log(keys);
const keys1 = Object.getOwnPropertyNames(person1);
console.log(keys1);
3.原型的动态性
在上边的内容中,我们已经明白,JavaScript中寻找属性或方法是通过搜索来实现的,因此我们可以动态的为原型添加属性和方法。这一方面没什么好说的,但有一点值得注意,如果把原型修改为另一个对象,就会出现问题。,还是先看一个实例:
function Person() {
}
const person = new Person();
Person.prototype = {
constructor: Person,
name: "James",
sayName: function () {
console.log(this.name);
}
};
console.log(person.sayName()); // 会报错
上边的代码会报错,根本原因是对象的原型对象指向了原型,而不是指向了构造函数,这就好比这样的代码:
var person1 = person;
var person2 = person;
person1 = son;
上边的代码中,person1换了一个对象,但是person2依然指向了person。用下边这个图开看更直接
4.原生对象的原型
这一小节是一个很重要的小结,我们慢慢的增加了对JavaScript语言的理解。原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object,Array,String等等)都在器构造函数的原型上定义了方法。
alert(typeof Array.prototype.sort); // => function
因此我们就通过这种手段为原生的引用类型扩展更多的属性和方法。
String.prototype.startsWith = function (text) {
return this,indexOf(text) == 0;
}
这种方式非常像面向对象语言中的分类,分类使用好了,能够增加程序的可读性,但在JavaScript中,不建议用这种方法为原生对象做扩展。因为这么做的后果是可能让程序失控。