面向对象有两个基本概念:
不过,在老版本的JavaScript中,这个概念需要改一改。老版本JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。基于原型实现对象模型比较简单,但是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要大量代码,且需要正确实现原型链。于是 ES6 开始 class 被正式引入 js。
js 使用 class 关键字来定义一个类,类体在一对花括号 {} 中。需注意的是,由于不是所有的主流浏览器都支持ES6的 class,所以如果需要使用 class,可以试试Babel这个工具将 class 代码转换为传统的 prototype 代码。
由类创建对象时自动执行的方法就是构造函数,js 的类在定义时可以定义特殊方法 constructor()
这个方法就是构造函数。
class ClassName {
constructor() { ... }
}
当类定义好后,可以使用 new 关键字来创建类的对象。对象就是类的实例化,当创建对象时,会自动执行其构造函数。
js 类继承使用 extends 关键字。
// 基类
class Animal {
// eat() 函数
// sleep() 函数
};
//派生类
class Dog extends Animal {
// bark() 函数
};
使用 super()
方法调用父类的构造函数。可以通过在构造函数中调用 super()
方法,调用父类的构造方法,来访问父类的属性和方法,继承父类的属性和方法。
使用 static 关键字修饰方法,就成为静态方法,也叫类方法。它属于类,但不属于对象。可以使用 类名.静态方法名
来调用静态方法。静态方法不能在对象上使用,只能在类中使用。
函数在声明时马上调用,就是立即执行函数。通常会定义一个匿名函数并执行,也只执行一次。
立即执行函数的格式有很多种,但是不同浏览器貌似支持的写法不太一样,有一个较为通用的写法,是将函数定义过程放在一个括号内,并且再加一对括号,表示调用
// 通常写法
(function () {alert('匿名函数立即调用')})();
// 箭头函数写法
(() => {alert('匿名函数立即调用')})();
也有种简单写法,在函数前加叹号
!function() {alert('匿名函数立即调用')}();
// 此写法不能用于箭头函数
有这样一段js代码:
let p = prompt('input');
if (p) {
console.log(p);
}
通常来说,不能直接在 if 语句的条件部分进行变量声明和复制,但是可以使用立即执行函数表达式来模拟类似的效果:
if ((p = prompt('input'))) {
console.log(p);
}
这里赋值表达式会直接返回函数的结果进行判断。需要注意的是,这种写法虽然可以工作,但是某些情况下可能会造成作用域混淆。
对象内部的函数成员(即绑定到对象的函数)就是方法。使用上和函数类似,不同的地方在于 this 关键字。this 是一个特殊变量,它始终指向当前对象。当函数没有自身对象时 this 就是全局对象,即 window 对象。比较坑爹的是,下面这个例子即使拿到函数再调用时,this 也是指向的 window
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
var fn = xiaoming.age; // 先拿到xiaoming的age函数
fn(); // NaN
要保证 this 指向正确,必须用 obj.xxx() 的形式调用!由于这是一个巨大的设计错误,要想纠正可没那么简单。ECMA决定,在strict模式下让函数的 this 指向 undefined ,因此,在strict模式下,上例中 fn() 会得到一个错误 Uncaught TypeError: Cannot read property 'birth' of undefined
,将此问题暴漏出来。另外在重构函数中也有类似的问题
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - this.birth;
}
return getAgeFromBirth();
}
};
xiaoming.age(); // Uncaught TypeError: Cannot read property 'birth' of undefined
原因是 this 指针只在 age 方法的函数内指向 xiaoming ,在函数内部定义的函数, this 又指向 undefined 了!(在非strict模式下,它重新指向全局对象 window !)。修复的办法是用一个 that 变量首先捕获 this
var xiaoming = {
name: '小明',
birth: 1990,
age: function () {
var that = this; // 在方法内部一开始就捕获this
function getAgeFromBirth() {
var y = new Date().getFullYear();
return y - that.birth; // 用that而不是this
}
return getAgeFromBirth();
}
};
xiaoming.age(); // 25
也可以使用函数本身的 apply 方法来手动控制 this 指向的对象。它接收两个参数,第一个参数就是需要绑定的 this 变量,第二个参数是 Array ,表示函数本身的参数。
function getAge() {
var y = new Date().getFullYear();
return y - this.birth;
}
var xiaoming = {
name: '小明',
birth: 1990,
age: getAge
};
xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空
或是 call 方法,区别在于参数按照顺序传入。对普通函数调用,我们通常把 this 绑定为 null。
Math.max.apply(null, [3, 5, 4]); // 5
Math.max.call(null, 3, 5, 4); // 5
利用 apply() ,我们还可以动态改变函数的行为。
var count = 0;
var oldParseInt = parseInt; // 保存原函数
window.parseInt = function () {
count += 1;
return oldParseInt.apply(null, arguments); // 调用原函数
};
JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数
function add(x, y, f) {
return f(x) + f(y);
}
最经常用到的就是数组的一系列遍历操作:
map 就是映射,可以将若干元素以一一对应的关系传入函数中进行处理。由于 map() 方法定义在JavaScript的 Array 中,我们调用 Array 的 map() 方法,传入我们自己的函数,就得到了一个新的 Array 作为结果。注意: map() 传入的参数是 pow ,即函数对象本身。
function pow(x) {
return x * x;
}
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
var results = arr.map(pow);
console.log(results); // [1, 4, 9, 16, 25, 36, 49, 64, 81]
// 或者一些复杂的函数可以简单化
arr.map(String); // ['1', '2', '3', '4', '5', '6', '7', '8', '9']
再看reduce的用法。Array的 reduce() 把一个函数作用在这个 Array 的 [x1, x2, x3…] 上,这个函数必须接收两个参数, reduce() 把结果继续和序列的下一个元素做累积计算,其效果就是
[x1, x2, x3, x4].reduce(f) = f(f(f(x1, x2), x3), x4)
比方说对一个 Array 求和,就可以用 reduce 实现
var arr = [1, 3, 5, 7, 9];
arr.reduce(function (x, y) {
return x + y;
}); // 25
filter也是一个常用的操作,它用于把 Array 的某些元素过滤掉,然后返回剩下的元素。和 map() 类似, Array 的 filter() 也接收一个函数。和 map() 不同的是, filter() 把传入的函数依次作用于每个元素,然后根据返回值是 true 还是 false 决定保留还是丢弃该元素。
var arr = [1, 2, 4, 5, 6, 9, 10, 15];
var r = arr.filter(function (x) {
return x % 2 !== 0;
});
r; // [1, 5, 9, 15]
可见用 filter() 这个高阶函数,关键在于正确实现一个“筛选”函数。filter() 接收的回调函数,其实可以有多个参数。通常我们仅使用第一个参数,表示 Array 的某个元素。回调函数还可以接收另外两个参数,表示元素的位置和数组本身
var arr = ['A', 'B', 'C'];
var r = arr.filter(function (element, index, self) {
console.log(element); // 依次打印'A', 'B', 'C'
console.log(index); // 依次打印0, 1, 2
console.log(self); // self就是变量arr
return true;
});
JavaScript的 Array 的 sort() 方法就是用于排序的,但是排序结果可能让你大吃一惊
// 看上去正常的结果:
['Google', 'Apple', 'Microsoft'].sort(); // ['Apple', 'Google', 'Microsoft'];
// apple排在了最后:
['Google', 'apple', 'Microsoft'].sort(); // ['Google', 'Microsoft", 'apple']
// 无法理解的结果:
[10, 20, 1, 2].sort(); // [1, 10, 2, 20]
第二个排序把 apple 排在了最后,是因为字符串根据ASCII码进行排序,而小写字母 a 的ASCII码在大写字母之后。第三个排序结果是因为 Array 的 sort() 方法默认把所有元素先转换为String再排序,结果 ‘10’ 排在了 ‘2’ 的前面,因为字符 ‘1’ 比字符 ‘2’ 的ASCII码小。
sort() 方法的默认排序规则很坑,幸运的是,它是一个高阶函数,可以接收一个比较函数来实现自定义排序。通常规定,对于两个元素 x 和 y ,如果认为 x < y ,则返回负值 ,如果认为 x == y ,则返回 0 ,如果认为 x > y ,则返回正值。这样按照数字大小排序应该为
var arr = [10, 20, 1, 2];
arr.sort(function (x, y) {
if (x < y) return -1 ;
if (x > y) return 1;
return 0;
});
console.log(arr); // [1, 2, 10, 20]
需要注意的是,sort() 方法会直接对 Array 对象进行修改。
every() 方法可以判断数组的所有元素是否满足测试条件,类似于 filter ,只是不进行筛选而是根据条件返回 True 和 False。
find() 方法可以用于查找符合条件的第一个元素,找到返回元素,否则返回 undefined
findIndex() 和 find() 类似,也是查找符合条件的第一个元素,不同之处在于 findIndex() 会返回这个元素的索引,如果没有找到,返回 -1
forEach() 和 map() 类似,它也把每个元素依次作用于传入的函数,但不会返回新的数组。 forEach() 常用于遍历数组,因此,传入的函数不需要返回值
ES6标准新增了一种新的函数:Arrow Function(箭头函数)。箭头之前的部分是函数参数,之后的部分是函数体。例如
function (x) {
return x * x;
}
// 等价于
(x) => { return x * x }
// 如果只有一个参数,可以省略参数的括号
x => { return x * x }
// 如果函数体只有一条语句,则可以省略函数体的花括号和 return,能够自动返回其值
x => x * x
箭头函数相当于匿名函数,并且简化了函数定义。箭头函数有两种格式,一种像上面的,只包含一个表达式,连 { … } 和 return 都省略掉了。还有一种可以包含多条语句,这时候就不能省略 {… } 和 return。如果参数不是一个,就需要用括号 () 括起来
// 两个参数:
(x, y) => x * x + y * y
// 无参数:
() => 3.14
// 可变参数:
(x, y, ...rest) => {
var i, sum = x + y;
for (i=0; i<rest.length; i++) {
sum += rest[i];
}
return sum;
}
需注意返回一个对象,也需要使用括号括起来
x => { foo: x } // SyntaxError
// 应写为
x => ({ foo: x })
箭头函数看上去是匿名函数的一种简写,但实际上,箭头函数和匿名函数有个明显的区别:箭头函数内部的 this 是词法作用域,由上下文确定。箭头函数完全修复了 this 的指向, this 总是指向词法作用域,也就是外层调用者 obj
var obj = {
birth: 1990,
getAge: function () {
// 省略了 var that = this;
var b = this.birth; // 1990
var fn = () => new Date().getFullYear() - this.birth; // this指向obj对象
return fn();
}
};
obj.getAge(); // 25
generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。它借鉴了Python的generator的概念和语法。generator和函数不同的是,generator由 function* 定义(注意多出的 * 号),并且,除了 return 语句,还可以用 yield 返回多次
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
例如编写一个产生斐波那契数列的生成器函数
function* fib(max) {
var
t,
a = 0,
b = 1,
n = 0;
while (n < max) {
yield a;
[a, b] = [b, a + b];
n ++;
}
return;
}
直接调用一个generator和调用函数不一样, fib(5) 仅仅是创建了一个generator对象,还没有去执行它。
调用generator对象有两个方法