JS作为纯函数式编程语言,理解起来有些不习惯,这里记录下。
在c++、java中,默认情况下,变量声明后都是块作用域,即如下代码是无效的:
if(1){
int a = 20;
}
print(a);
用{}包括的部分即是块,a是在块内定义,块外不能调用。
但JS不一样,ES5时代,var声明的变量是函数作用域,声明的变量在整个函数内都有效,块并不能限制变量的作用域,这样不可避免就会存在代码污染,于是在ES6时代就有let来表示块作用域的对象。
变量类型 | 作用域 | 是否可被改变 |
---|---|---|
var | 函数作用域 | ✔️ |
let | 块作用域 | ✔️ |
const | 块作用域 | ❌ |
使用 == 进行比较时,如果变量类型不同,那么先转化类型,再比较。
比如字符串和数字比较时,先将字符串转化为数字,再比较。
=== 则表示严格相等,这种情况下,变量类型必须相等。
在JS中,函数是一等公民,它和其他语言中的变量一样,可以被赋值,作为参数,面向对象语言中一切皆对象,函数式编程语言中则是一切皆函数,于是在JS中可以有如下写法:
var f = function(){
console.log('hello world');
}
这里生成了一个匿名函数,并将其赋值给了变量f。
使用函数式编程可以减少代码量,减少出错的几率,将species为dog的对象筛选出来,使用非函数编程有如下写法:
let animals = [
{name:'a', species:'dog'},
{name:'b', species:'cat'},
{name:'c', species:'cat'},
{name:'d', species:'dog'},
];
let dogs = [];
for (let i=0; i<animals.length; i++){
if (animals[i].species === 'dog'){
dogs.push(animals[i])
}
}
使用函数式编程,则简洁一些,核心代码少了一半,也不容易出错,这里使用了 filter 函数。
dogs = animals.filter(function (item) {
return item.species === 'dog';
});
//dogs = [ { name: 'a', species: 'dog' }, { name: 'd', species: 'dog' } ]
类似的还可以使用 map 函数将所有动物的名字加入一个数组中
names = animals.map(function (item) {
return item.name;
});
//names = [ 'a', 'b', 'c', 'd' ]
上面的代码如果使用 箭头函数 (es6) 还能进一步化简,省略大括号和return语句:
names = animals.map(item => item.name);
更简洁的代码意味着出错的可能性更低,对于加载与浏览器的JS,还意味着节省字符加载数。
map是数组与数组间的映射关系,如果要做聚合操作,则需要用到 reduce
let persons = [
{age:20},
{age:39},
{age:22},
{age:12}
];
//输入函数接收两个值,一个是上次迭代的返回值,一个是这次迭代传入的值,第二个参数是迭代的初始值
let sum = persons.reduce((sum,item) => sum+item.age, 0);
对于c++来说函数不仅是一段可复用的代码,还代表一个闭包,闭包的特性使函数可以引用定义在函数外部的变量:
let num = 10;
function f() {
console.log(num);
}
num = 20;
f();//输出20
这里需要提一下,闭包和iOS中block的概念是不一样的,block是对外部变量进行截取复制,因此block之后,如果对变量值进行改变,并不影响block中截取的值。而JS中是获取了外部变量,因此上面的函数会输出20。
闭包的用处
function sendRequest(){
let requestID = '123';
$.ajax({
url: '/myUrl',
success:function(response){
console.log(requestID)
}
});
}
上面的代码,在其他语言中,由于ajax作为网络请求是异步操作,在调用的时候requstID可能已经被释放了,此时在调用就会出现错误。但JS中,由于闭包的特性,requstID仍然可以调用。再举个闭包对变量截取的例子:
function f() {
let num = 10;
return function() {
let a = 10;
console.log(a+num)
}
}
//这里f()在调用后就会对其包含的局部变量进行释放,但是闭包截取了其中的局部变量num
let b = f();
b();
由于闭包的特性,函数在声明后可以直接调用,如下面这种写法:
(function (i) {
console.log(i);
})(2);
curry 的概念:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数
function add(a, b) {
return a+b;
}
//curry化的add函数
function add2(a) {
return function (b) {
return a+b;
}
}
console.log(add(1,2));
console.log(add2(1)(2));
promise是异步编程新的解决方案,它封装了异步操作,并可获取该操作的结果,它的本质是一个构造函数。
使用promise可以避免callback hell(回调地狱)。
promise有三个状态,两种状态转换,一个promise只会改变一次状态:
pending => resolved
pending => reject
其中pending表示未确定的,resolved表示成功,reject表示失败。
例子:
//新建promise,并用它封装了一个异步操作,构造函数接收一个函数作为参数
//resolve和reject两个函数在Promise的构造函数中已经定义好了,会直接传入函数参数中
//其实这里resolve用于接收成功的回调值,reject用于接收失败的回调值
const p = new Promise((resolve, reject)=>{
setTimeout(()=>{
const time = Date.now();
if (time % 2 == 0){
resolve('成功回调' + time);
} else {
reject('失败回调'+time);
}
},1000);
});
//then方法将在异步操作执行完成后调用,参数为两个函数,分别用于处理成功操作和失败操作
//其中失败操作也可以使用catch处理,catch的使用方式看后面的例子
//之前接收的回调值在这一步将作为参数传入
// !!! 相比于之前的回调,then方法的回调并不需要异步操作开始之前就指定,可以在异步操作完成之后指定,这也是promise的一个优点,经常被人忽略。
p.then(value => {
console.log(value);
}, value=>{
console.log(value);
});
callback hell
下面的函数,每个函数需要以上层函数的执行结果为基础运行
每个函数的执行又有成功和失败两种状态
层层嵌套后的代码非常难以理解,被称为callback hell(回调地狱)
f1(function(res1){
f2(function (res1,res2) {
f3(function (res2, res3) {
console.log(res3);
},failureCallback);
},failureCallback);
}, failureCallback);
上面的回调地狱使用promise可以修改成如下模式:
//then内函数的返回可以是promise 值 或者抛出异常
f1.then(function (res1) {
return f2(res1);
}).then(function (res2) {
return f3(res2);
}).then(function (res3) {
console.log(res3);
}).catch(failureCallback);
resolve和reject
这是两个语法糖
p1、p2含义是一样的,表示直接返回成功执行
p3、p4含义是一样的,表示直接返回失败执行
const p1 = new Promise((resolved, reject)=>{
return resolved(1);
});
const p2 = Promise.resolved(1);
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
const p3 = new Promise((resolved, reject)=>{
return reject(1);
});
const p4 = Promise.reject(1);
all和rece
如果现在有多个异步操作,全部执行成功后再执行下面的操作,这个时候可以使用all
const p1 = Promise.resolve(1);
const p2 = Promise.resolve(2);
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(values => {
console.log(values); //这里打印[1,2,3],顺序和前面Promise顺序相同
}).catch(reson => {
console.log(reson);
});
如果要输出多个Promise中最快完成的那个,则可以使用race:
const p1 = new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve(1);
},100);
});
const p2 = new Promise(((resolve, reject) => {
setTimeout(()=>{
resolve(2);
},200);
}));
Promise.race([p1, p2]).then(value => {
console.log(value); //这里输出1,因为1执行快些
});
promise的一些细节
promise中抛出异常?
状态直接变成reject,reason就是抛出的值
promise中绑定了多个回调(then的第一个函数参数),和多个错误处理(catch和then的第二个函数参数)的时候是如何调用的呢?
绑定的所有回调和错误处理,在状态改变的时候都会调用。
这样嵌套就编程了链式调用,所有的异常都利用catch捕获,但对于回调地狱更好的解决方式可能是async和await。
这种方式比promise更加简洁,直接没有了回调函数。
//await和async
async function request(){
try{
const res1 = await f1();
const res2 = await f2(res1);
const res3 = await f3(res2);
console.log(res3);
}catch(err){
console.log(err);
}
}
使用语法创建一个对象:
p = {
'name':tang,
age,29,
run:function(){
}
}
//对象中属性的访问有两种方式:
p.age;
p['age'];
使用构造函数创建一个对象,这里函数Person就是Person类的构造函数
function Person(){
this.name = 'tang';
this.age = '29';
this.run = function () {
console.log(this.name+'is run');
}
}
let p1 = new Person();
p1.run();
使用Object创建一个对象:
let p = new Object();
p.name = 'tang';
p.age = 29;
p.run = function(){};
let p = new Object({
name:'tang',
age:29,
run:function(){};
});
//Fn直接使用时,为函数对象,函数对象使用即为函数调用,使用()运算符的都是函数对象
function Fn() {
console.log('Fn');
}
//使用new产生的是实例对象,此时Fn是构造函数,实例对象使用点语法来调用属性和成员函数
let fn = new Fn();
fn.name = 'hyt';
JS和传统面向对象语言不同,所有实例对象都有原型对象,从原型对象继承属性和方法,原型对象也有原型对象,这种一层层的关系叫做原型链。
function Person(){
//当Person作为普通函数时,this指向window
//当Person作为构造函数时,this指向生成的实例对象
//使用这种方式定义的属性在生成实例对象时会直接拷贝到实例对象中
this.pName = 'goldfish';
}
Person.prototype.pName2 = 'goldfish2'; //使用这种方式定义的属性不会被拷贝到实例对象中,而是遵循原型链查找的原则
一般来说,属性定义到构造函数中,函数定义到原型对象中,这样可以节省内存~。
js中所有函数在调用的时候都会传入this对象,如果是node.js,直接调用的函数this指向一个空对象{},如果是浏览器中,this指向window对象。
apply、call、bind的作用本质上都是用于改变this指针的指向