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__我们看一张图:

JavaScript高级编程小结_第1张图片

上图很好的展示了构造函数和实例对象之间原型的关系。我们在这里就不一一说明了。虽然我们通过__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。用下边这个图开看更直接
JavaScript高级编程小结_第2张图片

4.原生对象的原型

这一小节是一个很重要的小结,我们慢慢的增加了对JavaScript语言的理解。原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型,都是采用这种模式创建的。所有原生引用类型(Object,Array,String等等)都在器构造函数的原型上定义了方法。

alert(typeof Array.prototype.sort); // => function

因此我们就通过这种手段为原生的引用类型扩展更多的属性和方法。

String.prototype.startsWith = function (text) {
    return this,indexOf(text) == 0;
}

这种方式非常像面向对象语言中的分类,分类使用好了,能够增加程序的可读性,但在JavaScript中,不建议用这种方法为原生对象做扩展。因为这么做的后果是可能让程序失控。

你可能感兴趣的:(JavaScript高级编程小结)