js笔记
1. 关于循环
- for( A ) { B };
for循环的A和B是两个不同的作用域,B作用域是A作用域的子域,如下面的代码
for(let i = 0; i < 3;i++){
let i = "han";
console.log(i);
}
// => han
// han
// han
在上面的代码中
- 在B位置,i可以被重新声明,说明A和B是两个不同级的作用域
- 虽然在B作用中,i被重新赋值,但仍然不影响A作用域中,i作为循环变量来使用
这说明在B作用域中的操作对A无影响,因此B为A的子作用域
2. 变量提升(没有块级作用域)的问题
- 内层变量覆盖外层变量
var tmp = new Date();
function f(){
console.log(tmp);
if(false){
var tmp = 'hello world';
}
}
// => undefined
如上代码,本来console.log()输出的应该是全局的变量tmp,即当前的时间,但由于if语句中的tmp变量
提升,覆盖了全局变量tmp的值,输出为undefined
- 循环变量泄漏为全局变量
var s = 'hello';
for (var i = 0; i < s.length; i++) {
console.log(s[i]);
}
console.log(i);
// => 5
上面代码中,变量i本来只属于循环,但由于变量提升,它被声明为了全局变量
3. 变量声明
es6 有6种变量声明的方法
var(es5) function(es5) let const import class
js 的七种数据类型
string boolean number null undefined Object Symbol
4. 一些新增的常用的字符串处理方法
let s = "Gaarahan";
//1. 内容查询
s.startsWith('Gaa'); //true
s.endsWith('han'); //true
s.includes('aha'); //true
//第二个参数表示开始搜索的位置
{
//这俩搜索的是从0到第二个参数n
s.startsWith('Gaa',1); //false
s.includes('aha',s.indexOf('aha') ); //true
// 这个搜索的是前n个字符
s.endsWith('han',s.length - 2); //false
s.endsWith('han',s.length); //true
}
//2. 重复
'han'.repeat(3); // "hanhanhan"
'han'.repeat(3.6); // "hanhanhan"
//3. 字符串补全
s.padStart();
s.padEnd();
'Gaara'.padStart(8,'han') //"hanGaara"
'Gaara'.padStart(6,'han') //"hGaara"
'Gaara'.padStart(5,'han') //"Gaara"
'Gaara'.padStart(7) //" Gaara" 无第二参数,默认空格补全
5. 正则的方法
- RegExp.exec("string")
返回给定字符串中正则表达式的一个匹配的结果数组,每次匹配会从上次匹配的结果处继续
let str = "aaa_aa_aaaa";
let r1 = /a+/g;
r1.exec(str);
... //多次运行的结果为 => ["aaa"] , ["aa"], ["aaaa"] , null, ["aaa"] , ["aa"] ...
注意必须将上面的正则表达式创建为一个变量,才能每次都从上次匹配的结果处继续
let str = "aaa_aa_aaaa";
/a+/g.exec(str);
// => 多次运行结果为 ["aaa"] ["aaa"] ["aaa"] ["aaa"] ..
// 始终指向第一处匹配,因为每一次都是用的一个新的正则表达式,其中并不会存储上次的
// 查询位置
6. 关于数字
- 在js中,整数和浮点数采用的是相同的存储方式,因此有
Number.isInteger(25); //true
Number.isInteger(25.0); //true
Number.isInteger(25.1); //false
- JS能够准确表示的数在2^53 ~ -2^53之间
//超过范围的数字无法准确判断
Math.pow(2,53) === Math(2,53) + 1; // true
- 通常理解的向上取整ceil()和向下取整floor()都可以理解为在数轴上的上和下,注意负数
Math.ceil(4.9) // 5
Math.floor(4.9) // 4
Math.ceil(- 4.9) // -4
Math.floor(- 4.9) // -5
7. 尾调用优化与递归
- 尾调用优化特性是es6提供的一个优化方式,当一个函数的最后一步操作是调用另一个函数,而且
该调用无需再使用外层函数中的内部变量时,就被称为尾调用
let x = ()=>{
let i = 1;
return g(i); //尾调用
}
let y = ()=>{
let i = 1;
let j = 2;
let g = (b) = >{
return b + j;
}
return g(i);
//不是尾调用,除了传参之外,还使用了外部函数的内部变量,此时函数y的调用帧不能被删除
}
提出尾调用的目的是为了优化代码,函数中的嵌套调用的过程中,我们将当前的主函数的状态
(子函数的调用位置,以及主函数中的内部变量)保留,为子函数以及其参数重新创建一个调用帧
(call frame),当子函数调用完成后,我们才删除这个调用帧,并将结果带回主函数,继续我们主函
数的调用帧。
但是,对于尾调用来说,我们已经不再需要主函数中的调用位置以及各种内部变量了,返回到主函
数后,我们唯一需要做的就是将子函数的返回值再做一次返回,如下:
//两个定义好的函数,y尾调用了z
function y(){
let i = 3;
return z(3);
}
function z(i){
return i*2;
}
//函数x
function x(){
...
y();
...
}
// 优化后相当于:
function x(){
z(3);
}
从上面的函数,我们可以看出,尾调用的优化是很有必要的,而优化的过程可以这样简单理解:
对于第一个x函数,我们调用了函数y,为函数y创建了一个调用帧,而函数y又尾调用了函数z
我们知道,尾调用的函数z已经不需要再使用函数y中的调用位置了(因为z是最后一步操作,
它在y中的下一步操作就是返回到函数x中去),同时函数y中的内部变量也已经通过传值的方式
保存了下来,那么函数y的调用帧已经没有存在的必要了(我们可以直接把函数z返回到主函数x
中去,而不需要返回到y,再由y返回到x)
此时,我们可以删除函数y的调用帧,用内部函数z的调用帧取代函数y的调用帧,就可以节省
一部分调用栈(call stack)的空间
- 在简单理解了尾调用优化的原理后,看看尾调用优化的用处:
我们已经知道,尾调用优化后,我们可以使用一个尾调用函数的调用帧来替代原来的两层调用帧
那么,当我们有一堆嵌套的尾调用函数时,我们就可以保证始终只存在一个调用帧,例:
function a(){
let i = 1;
return b(i);
}
function b(i){
let j = i + 2;
return c(j);
}
function c(j){
return j;
}
如上,函数a,b都使用了尾调用,当我们调用函数a时,为函数a创建了一个调用帧,接着当我们尾调用
函数b时,我们使用函数b的调用帧替代了函数a的调用帧,此时调用栈中仍然只存在一个调用帧b,而
当我们继续执行,调用函数c时,我们又使用函数c的调用帧来替代调用栈中唯一的调用帧b
- 常见的应用:
我们在单个的函数中可以使用尾调用来减小函数的内存占用,但这些一个个写出来的尾调用函数
优化的效果并不显著,我们平时使用的最消耗内存的做法便是递归调用,而尾调用优化能大大的
优化递归调用占用的内存,但我们需要将递归函数改写成尾调用的形式
//原始的递归求阶乘代码
function func1(i){
if(i === 1) return 1;
return func1(i - 1) * i;
}
// 改写为尾调用函数
function func2(i,temp){
'use strict'
if(i === 1){
return temp;
}
temp *= i;
return func2(i - 1,temp);
}
//在浏览器控制台运行
func1(100000); // 栈溢出
func2(100000,1); // 栈溢出 ???
// 2. 原始的菲波纳切函数
function fe1(i){
if (i <= 1)
return 1;
return f(i - 1) + f(i - 2);
}
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity
8. call apply bind
- 注意,箭头函数的this指向是固定的,始终指向函数定义时的对象,而不是使用时的对象
无法使用这三个函数来为其指定this的指向
let a = {x:3};
let func = ()=>{
console.log(this);
//箭头函数没有自己的this,它的this是该函数外层代码块的this
// 在此处是window对象
}
func(); //=> window
//call()
func.call(a); // =>window
//apply
func.apply(a); // => window
// bind()
let func2 = func.bind(a);
console.log(func2);// => ()=>{console.log(this);}
func2(); // window
1.function.call(thisArg,arg1,arg2, ...)
每个函数都有自己的this指向,使用call方法可以为我们调用的函数指定其this指向
function con(x){
console.log(this.a);
}
con(1); // => undefined
//使用call函数为其指出this指向
con.call({a:2},1); // => 2
- 当处于非严格模式时,指定的thisArg为 null undefined时,this会自动指向
全局对象(浏览器为window,node为global)
function con(){
console.log(this); // => Window对象
return 1;
}
let a = con.call(null);
console.log(a); // 1
- 我们可以使用call方法来实现继承
在JAVA中,我们给出一个Product类,声明类Food继承该Product类
class Product{
protected String name;
protected String price;
public Product(String name,String price){
this.name = name;
this.price = price;
}
}
class Food extends Product{
private String kind;
public Food(String name,String price,String kind){
super(name,price);
this.kind = kind;
}
}
Food apple = new Food("apple","15","friut");
在js中,我们可以借助call这样来写
function Product(name,price){
this.name = name;
this.price = price;
}
function Food(name,price,kind){
Product.call(this,name,price);
this.kind = kind;
}
let apple = new Food('apple','15','friut');
console.log(apple); // => Food{name:'apple',price:'15',kind:'friut'};
对比上面的写法,我们在js中使用了call来调用函数Product,类似与在java中使用super()
函数来调用父类的构造方法
- call的一个比较常见的用法是在处理类数组对象时,我们可以通过call函数,让其调用
数组的方法,完成我们需要的操作详细讲解
例如将类数组对象arguments转化为数组:
let argsArr = Array.prototype.slice.call(arguments);
使用forEach方法来遍历HTMl collection
[].forEach.call(htmlCol,(val,index,arra=>{
console.log(val);
}))
-
function.apply(thisArg,[args])
apply的用法和call差不多,区别在与apply所调用函数的参数是以数组或者类数组方式提供的
也就是说,call中能使用的地方,都可以使用apply来替代,有时apply写的还更简单
- 上面的继承使用apply改写
function Product(name,price){
this.name = name;
this.price = price;
}
function Food(name,price,kind){
Product.apply(this,arguments);
//这里直接将Food函数的arguments对象传过去,虽然多传了一个参数kind,但在写法上更简单
this.kind = kind;
}
let apple = new Food('apple','15','friut');
console.log(apple); // => Food{name:'apple',price:'15',kind:'friut'};
- 将一个数组push到另一个数组中去(使用concat会创建新数组,push不会)
let a = [1,2,3];
let b = [4,5,6];
[].push.apply(a,b);
console.log(a); // => [1,2,3,4,5,6]
- 使用max函数找出一个数组中的最大值
let a = [1,2,3,45,6,7];
let res = Math.max.apply(null,a);
console.log(res); // => 45
//另一种简化写法 : es6
Math.max(...a); // => 45
-
function.bind(thisArg,arg1,arg2 ...)
bind函数不是一次原函数的调用,它会返回一个新的函数,具有指定的this指向,并且在
调用新函数时,在bind时指定的参数会作为新函数的前几项参数值
let a = {x:3};
function func(){
console.log(this.x);
}
func(); //=> undefined
let func2 = func.bind(a);
console.log(func2);// => ()=>{console.log(this.x);}
func2(); // =>3
关于继承的常用方法
a instanceof A // 判断a是否为A的实例
Object.getPrototypeOf(a) === A // 获取实例a的原型
Object.getPrototypeOf(Gaara) === Person // 获取子类Gaara的父类,判断继承