美文网首页
JavaScript忍者秘籍

JavaScript忍者秘籍

作者: Ethan_lcm | 来源:发表于2018-11-23 15:30 被阅读0次

函数调用

构造器

构造器(constructor)被调用时候会发生:

  1. 创建一个新对象
  2. 传递给构造器的对象是this参数,从而成为构造器的函数上下文
  3. 如果没有显式的返回值,新创建的对象则作为构造器的返回值进行返回
var a = new B();
// 相当于
// 1. 创建一个新对象
var obj = {};
// 2. 指定__proto__属性
obj.__proto__ = B.prototype;
// 3. 指定构造函数
obj.constructor = B;
// 4. this指针更正
B.call(obj);
// 5. 返回新创建的对象
return obj;

this的指向问题

函数调用方式之间的主要差异是:作为this参数传递给执行函数的上下文对象之间的区别:

  1. 作为方法调用,this即上下文对象指向方法的拥有者
var a = {
    getname(){}
}
a.getname();
  1. 作为全局对象进行调用,this指向window。
  2. 作为构造器进行调用,this指向新创建的对象实例。
  3. 被apply()和call()调用的时候,this会被apply和call指定到调用者上。也就是指定上下文
    重写个forEach案例
function forEach(list,callback){
    for(var i=0;i<list.length;i++){
        // 将当前元素作为第一个参数传入,当前索引作为第二个参数传入
        // 这就使得当前元素变成函数的上下文
        callback.call(this,list[i],i);
    };
}
var arr = [1,2,3,4,5];
forEach(arr,function(item,index){
    console.log(item,index);
})

挥舞函数

递归

检查字符串是否为回文

function fn(text){
    if(text.length<=1) return true;
    if(text.charAt(0) != text.charAt(text.length-1)) return false;
    return arguments.callee(text.substr(1,text.length-2))
}
var str = "1234567654321";
fn(str);
// 13个字符执行7次,没任何性能问题

Tips:“!!”两个感叹号可以将任意JS表达式转化为其等效布尔值的简单方法。

!!"he shot me down" === true
!!0 === false;

重载

利用apply和call查找数组中的最小值和最大值

function small(arr){
    return Math.min.apply(Math,arr);
}
function largest(arr){
    return Math.max.apply(Math,arr);
}
var arr = [1,2,23,4,5,6,7,78];
small(arr);
// 上述其实就等价于Math.min(1,2,23,4,5,6,7,78)

使用apply将arr的上下文指定给Math函数
重载其实就是让参数变的更多,比如多参数实现一个参数就实现

函数的length属性
申明函数都会有一个name和length属性,此处的length属性和arguments的length一样。
fn的length表示函数代码写法上接收到的参数,arguments表示函数实际调用中接收到的参数。

function fn(a,b,c,d,e){
    console.log(arguments.length);  // 2
}
fn(1,2);
console.log(fn.length); // 5

判断函数的length可以方便的去给函数定义重载,比如:

function fn(){
    switch(arguments.length){
        case 0:
            // do soemthing...
            break;
        default:
            break;
    }
}

闭包

书上的概念是:
闭包是一个函数在创建时
允许该自身函数访问并操作该自身函数之外的变量时
所创建的作用域。

var outerVal = "ninja";
var later;
function outerFunction(){
    var innerVal = "samurai";
    function innerFunction(param){
        console.log(outerVal);
        console.log(innerVal);
        console.log(param);
        console.log(tooLate);
    }
    later = innerFunction;
    // later被赋值的那一刻,innerFunction的作用域也被赋值给了later,所以最后later是能访问所有作用域下的变量
}
var tooLate = "I ma tooLate";
outerFunction();
later('aaa');

在innerFunction赋值给later的时候,不仅声明了一个函数,还创建了一个闭包,该闭包不仅包含函数申明,还包含了函数声明的那一时刻点上该作用域中的所有变量。
像保护气泡一样,只要innerFunction()函数一直存在,它的闭包就保持该作用域中即将被垃圾回收的变量。

闭包在计时器(timer)和回调(callback)中的作用

function animateIt(el){
    var elem = document.getElementById(el);
    var tick = 0;
    var timer = setInterval(function(){
        if(tick<100){
            elem.style.left = elem.style.top = tick + "px";
        }else{
            clearInterval(timer);
            console.log(tick);
            console.log(timer);
            console.log(elem);
        }
    },10)
}
animateIt('box');

上述代码中创建的每一个计时器setInterval都可以看成是一个保护泡。每个动画都有自己的“私有”气泡。

闭包不是在创建那一时刻点的状态的快照,而是当时一个真实状态的封装,连通作用域的一起的封装。

绑定函数上下文

// <button id="test">click me</button>
var button = {
    clicked:false,
    click(){
        this.clicked = true;
        console.log(button.clicked);    // false
        console.log(this.clicked);      // true
        console.log(this);              // <button id="test">click me</button>
    }
}
var elem = document.getElementById('test');
elem.addEventListener('click',button.click,false);

上述代码在执行button.click的时候就this的作用域指向的是当前点击的button。所以输出this.clicked的时候是true。而输出button.clicked的时候是false,因为this的指向问题。也就是函数的上下文环境已经变化了。

这种时候使用apply或者call强制绑定作用域就可以解决:

function bind(context,name){
    return function(){
        return context[name].apply(context,arguments);
    }
}
var button = {
    clicked:false,
    click(){
        this.clicked = true;
        console.log(button.clicked);    // true
        console.log(this.clicked);      // true
        console.log(this);              // 当前button对象
    }
}
var elem = document.getElementById('test');
elem.addEventListener('click',bind(button,"click"),false);
// 强制将click事件的作用域指向到button对象上

函数柯里化

假设想要将一个字符串分隔成CSV(逗号分隔),并忽略多余的空格。通过curry实现:

// 给function上定义一个原型方法
Function.prototype.curry = function(){
    var fn = this,args = Array.prototype.slice.call(arguments);
    return function(){
        var arg = 0;
        for(var i=0;args.length && arg < arguments.length;i++){
            if(args[i] === undefined){
                args[i] = arguments[arg++];
            }
        }
        return fn.apply(this,args);
    }
}

其实就是将函数中的agrguments收集起来组成一个数组然后再操作。

即时函数(自执行函数)

充分利用闭包的典范。

(function(){
     //...
})()

原型和面向对象

实例属性

prototype
使用new操作符将函数作为构造器进行调用的时候,其上下文被定义为新对象实例。

function Ninja(){
    this.swung = false;
    this.swingSword = function(){           // 优先级高
        console.log(1);                     // 输出1
        return !this.swung;
    }
}
Ninja.prototype.swingSword = function(){    // 优先级低
    console.log(2);                         // 不输出任何值
    return this.swung;
}
var n = new Ninja();
console.log(n.swingSword());                // true

这里在访问swingSword()的时候先已经在Ninja中找到对应方法,如果没有找到才会去prototype上查找。
在构造器内的绑定操作优先级永远都高于在原型上绑定操作的优先级。
将上述代码修改一下:

function Ninja(){
    this.swung = false;
}
var n = new Ninja();
Ninja.prototype.swingSword = function(){
    return this.swung;
}
console.log(n.swingSword());  // false

上述代码的过程:

  1. 在引用对象的一个属性时,首先检查该对象本身是否拥有该属性,如果有,则直接返回。
  2. 如果没有就查找该对象的prototype上是否有该属性,如果有,则返回,
  3. 如果该对象上的prototype也没有就在其父级(有的话)上的prototype上查找。
  4. 查找到了返回,没有查找到,就返回undefined。
    上述这么个寻找的过程就是原型链。

constructor
JS中的每一个对象,都有一个名为constructor的隐式属性,该属性引用的是创建该对象的构造器。由于prototype是构造器的一个属性,所以每一个对象都有一种方式可以找到自己的原型。

function Ninja(){}
var ninja = new Ninja();
console.log(ninja.constructor);     // ƒ Ninja(){}

constructor指向其本身的构造器。

使用构造器进行对象类型判断

function Ninja(){}
var ninja = new Ninja();
console.log(typeof ninja == 'object');      // true
console.log(ninja instanceof Ninja);        // true
console.log(ninja.constructor == Ninja);    // true

使用constructor重新创建一个实例

function Ninja(){}
var ninja = new Ninja();
var ninja2 = new ninja.constructor();
console.log(ninja2 instanceof Ninja);       // true
console.log(ninja2 !== ninja);              // true

上述实现了新实例的创建,算是深拷贝。

继承和原型链

创建原型链最好的方式是,使用一个对象的实例作为另外一个对象的原型。

SubClass.prototype = new SuperClass();
// 比如
var Person = function(){
    this.age = 19;
    this.name = "zhangsan"
}
function Tom(){};
Tom.prototype = new Person();
var ethan = new Tom();
console.log(ethan);   // Tom {},查找__proto__上有两个属性,age和name

这样Tom的原型上就继承了来自Person所有的共享的属性和方法。
既然这样可以为啥不能直接原型等于原型呢?

Tom.prototype = Person.prototype;

这样做就是一个浅拷贝,Person.prototype上的属性和方法变更的话就会影响到Tom,影响很大。

扩展对象

有时候看别人的代码会直接在对象或者数组的prototype原型上直接添加一个扩展方法:

Object.prototype.keys = function(){
    var keys = [];
    for(var p in this){
        console.log(this);      // {a:1,b:2,c:3}
        console.log(p);         // a,b,c,keys
        keys.push(p);
    }
    return keys;
}
console.log({a:1,b:2,c:3}.keys());  // [a,b,c,keys]4个字符串组成的数组

这种问题很难预防,这样操作会造成隐患。但是上述代码可以通过hasOwnProperty方法来解决。

Object.prototype.keys = function(){
    var keys = [];
    for(var p in this){
        if(this.hasOwnProperty(p)) keys.push(p);
    }
    return keys;
}
console.log({a:1,b:2,c:3}.keys());

hasOwnProperty是用来检查一个属性是对象实例上定义的还是从原型里导入的。
上述代码中的a,b,c都是在实例上定义的,keys方法是从原型中导入的。

原生对象的子类化

function MyArray(){}
MyArray.prototype.length = 0;
(function(){
    var methods = ["push","pop","shift","unshift","slice","splice","join"];
    for(var i=0;i<methods.length;i++)(function(name){
        MyArray.prototype[name] = function(){
            return Array.prototypep[name].apply(this,arguments);
        }
    })(methods[i])
})()

var mine = new MyArray();
mine.push(1,2,3);
console.log(mine);

实例化问题

普通函数和构造函数引用不确定的时候会造成的一些问题
污染当前作用域,通常是全局命名空间

function User(f,l){
    this.name = f+l;
}
var name = "aa";
var user = User("bb","cc");
console.log(name);      // name输出bbcc,而不是aa

User被执行时候设置了name的属性,而被调用时候的作用域是全局的。这些都是编码隐患,所以let和const的出现能有一些帮助。
这也体现了js的这种弱语言的特点。
那这种本该使用new操作符实例化的构造函数,缺被当做普通函数引用了的问题怎么避免呢?
使用instanceof来检测

function User(f,l){
    if(!(this instanceof arguments.callee)){
        return new User(f,l);
    }
    this.name = f+l;
}
var name = "aa";
var user = User("bb","cc");
console.log(name);      // aa

this instanceof arguments.callee 的结果为true时表示其是被new操作符实例化调用构造函数的。
为false表示只是普通函数的引用。

正则表达式

正则表达式申明

有两种方法:

  1. 正则字面量
var pattern = /test/;
  1. RegExp实例
var pattern = new RegExp("test");

除了表达式本身,还有三个标志可以与正则表达式进行关联。

  • i 让正则表达式不区分大小写,所有/test/i不仅可以匹配"test",还可以匹配"Test","TEST"等
  • g 匹配模式中的所有实例,而不是默认只匹配第一次出现的结果。
  • m 允许匹配多行,比如可以匹配文本区元素testarea中的值

精确匹配
/test/表示test必须连在一起被匹配

匹配一类字符
[abc]表示只需要匹配到a,b,c中任意一个字符
[^abc]表示需要匹配到 a,b,c之外的所有字符
[a-m]表示需要匹配到从字母a到m的所有字符,包括a和m。

转义
对特殊的字符需要进行转义
比如需要匹配“[^]”等特殊字符的时候就需要先将它们转义。 /\[\^]/用这种方式去匹配特殊字符。另外"\"是用于匹配反斜杠""的,也是正常的规则。

匹配开始和匹配结束
/^test/表示字符串必须以test开头
/test/表示字符串必须以test结尾 /^test/表示整个字符串只能是test

重复出现
/t?est/ 匹配“test”和“est”,?表示前面的字符可以出现一次或根本不出现
/t+est/ 匹配“ttest,tttest,ttttttest”等,但是不匹配“est”, + 表示一个字符出现一次或者多次
/a{4}/ 匹配连续出现aaaa的字符串
/a{4,10}/ 匹配任何含有“aaaa”到“aaaaaaaaaaa”,也就是又4或者10位a组成的连续字符串
/a{4,}/ 匹配4个或多于4个“a”组成的连续字符串
正则中的重复操作都是贪婪的,比如对“aaa”进行字符串匹配,/a+/将匹配所有这三个字符,而/a+?/表示只匹配一个a字符即可。所以平时的写法要注意一些性能优化。

预定义字符类
这个是用来检测一些键盘空格等操作符。举几个常见例子

预定义术语 匹配内容
\r 回车
\n 换行
\d 任意数字,等价于[0-9]
\D 任意非数字,等价于[^0-9]
\w 匹配包括下划线的任意单词字符,等价于[A-Za-z0-9_]
\W 匹配任何非单词字符,等价于[^A-Za-z0-9]
\s 匹配任何空白字符,包括空格、制表符、换页符
\S 匹配任何非空白字符
\b 匹配单词边界
\B 匹配非单词边界

特殊字符匹配

特别字符 描述
$ 匹配输入字符串的结尾位置
() 匹配一个子表达式的开始和结束位置。
* 匹配零次或多次。
+ 匹配一次或者多次
. 匹配除换行符\n之外的任何单字符
[ 标记一个中括号表达式的开始
? 匹配零次或一次,或指明一个非贪婪限定符
\ 匹配特殊字符。\ 表示匹配自身
^ 匹配字符串的开始位置,但是如果在[]方括号中使用,表示不接受该字符集合
{} 标记限定符表达式的开始
` `

或操作符(OR)
就是"|"表示或的关系。
/a|b/ 表示匹配字符a或者b
/(ab)+|(cd)+/ 表示匹配多次出现ab或多次出现cd字符串

编译正则表达式

正则表达式是一个多阶段的处理过程。其有两个重要阶段就是 编译执行

<div class="samurai ninja"></div>
<div class="ninja samurai"></div>
<div></div>
<span class="samurai ninja ronin"></span>
<script>
function findClassInElements(className,type){
    var elems = document.getElementsByTagName(type ||'*');
    var regex = new RegExp("(^|\\s)" + className + "(\\s|$)");
    var results = [];
    for(var i=0;i<elems.length;i++){
        if(regex.test(elems[i].className)){
            results.push(elems[i]);
        }
    }
    return results;
}
console.log(findClassInElements("ninja","div").length);
console.log(findClassInElements("ninja","span").length);
</script>

用全局表达式进行匹配

var html = '<div class="test"><b>Hello</b><i>world!</i></div>';
var res = html.match(/<(\/?)(\w+)([^>]*?)>/);
var all = html.match(/<(\/?)(\w+)([^>]*?)>/g);
console.log(res,all);

匹配解析:
/<>/ 匹配"<>"
/< (/?) >/ 匹配是否存在"/",检测</>这种的存在
/< (\w+) >/ 匹配中间div的名称,\w等价于[A-Za-z0-9_]\w+表示字符串是多个的存在的
/< ([^>]*?) >/ 匹配非>字符串,?表示前面非>字符有可能存在有可能不存在

使用exec

var html = '<div class="test"><b>Hello</b><i>world!</i></div>';
var tag = /<(\/?)(\w+)([^>]*?)>/g,match;
var num = 0;
while((match = tag.exec(html)) !== null){
    console.log(match[0]);
}

match和exec的用法不同:

var html = '<div class="test"><b>Hello</b><i>world!</i></div>';
var tag = /<(\/?)(\w+)([^>]*?)>/g;
var res = html.match(tag);
var res2 = tag.exec(html);

上述两者返回的结果相同。

replace中嵌入正则

将css样式写法由-改写成驼峰式,比如:border-bottom-width改成borderBottomWidth

var cls = "border-bottom-width";
var reg = /-(\w)/g; // 匹配“-”开头后面一个是[A-Za-z0-9_]的字符格式
var res = cls.replace(reg,function(all,letter){
    return letter.toUpperCase();
})
console.log(res);

将url字符串“foo=1&foo=2&foo=3&blash=4&foo=5”替换成“foo=1,2,3,5&blash=4”

var str = "foo=1&foo=2&foo=3&blash=4&foo=5";
var reg = /([^=&]+)=([^&]*)/g;
function compress(source){
    var keys = {},newStr="";
    source.replace(reg,function(full,key,value){
        if(!keys[key]){
            keys[key] = value;
        }else{
            keys[key] = [].concat(keys[key],value);
        }
    })
    for(var i in keys){
        if(Array.isArray(keys[i])){
            newStr += (i+'='+keys[i].join(',')+'&');
        }else{
            newStr += (i+'='+keys[i] + '&');
        }
    }
    return newStr.substring(0,newStr.length-1);
}
var newStr = compress(str);
console.log(newStr);

正则分析:/([^=&]+)=([^&]*)/g
/()=()/ 匹配所有的a=b的格式
/([^=&]+)=()/ 表示a部分包含"="和"&"特殊字符
/()=([^&]*)/ 表示不包含"&"特殊字符,不管是出现零次还是多次
其实上述代码完全可以写成:/([^=&])=([^=&])/

一些常见的正则引用

实现string.trim函数

function trim(str){
    var reg = /^\s+|\s+$/g;
    return (str || '').replace(reg,"");
}
console.log(trim("   dwada  dwad  dwa  "));

正则解析
/^\s+/ 这里"^"不是在[]中使用的,所以其是匹配第一个字符。\s表示匹配空格,\s+表示不止一个空格
/\s+/表示结尾
/^\s+|\s+/g 合起来的意思就是匹配所有的开始位置和结束位置都是空格且不知道一个空格的字符串,找到然后使用replace替换成功“”。这样就去除了收尾的空格。 如果去除收尾的"^",就表示将字符串中所有的空格都去掉。

匹配换行符

var html = "<div>Hello</div>\n<i>world!</i>";
var reg1 = /.*/;
var reg2 = /[\S\s]*/;
var reg3 = /(?:.|\s)*/;
console.log(reg1.exec(html)[0]);
console.log(reg2.exec(html)[0]);
console.log(reg3.exec(html)[0]);

正则分析:
/./ .表示匹配除换行符以外的所有字符,表示有另个或多个
/[\S\s]/ 表示任何非空字符和任何空白字符。这个效率最佳
/(?:.|/s)*/ 没看懂。。。

线程和定时器

代码求值机制

eval()

函数构造器

var add = new Function('a','b','return a+b');
console.log(add(3,4));

理解DOM特性和DOM属性

特性(attribute)是DOM构建的一个组成部分。
属性(property)是元素保持运行时信息的主要手段。

通过DOM方法和属性访问特性值

window.onload = function(){
    var div = document.getElementsByTagName("div")[0];
    div.setAttribute("id","div1");
    div.id ="div2";
    div.setAttribute("id","div3");
}

不老事件

绑定和解绑事件处理程序

在DOM2下,一般使用addEventListenerremoveEventListener事件来绑定和解绑事件。IE9以前的版本使用的是attachEvent()detachEvent()
大多数情况下,两种情况很类似,只有一个明显的例外:IE Model没有提供事件捕获阶段的监听方式,其只支持事件处理过程的冒泡阶段。
在冒泡阶段,事件将事件源传播到DOM根节点,而在捕获阶段,则是从DOM根节点遍历传播到事件源上。

文档就绪事件

document.ready 在文档树加载完毕之后就会执行
window.onload 在所有文档加载完成(包括图片等素材元素加载)之后再执行
下面是一个实现跨浏览器都兼容的DOM Ready监听事件

(function(){
    var isReady = false,contentLoadedHandler;
    function ready(){
        if(!isReady){
            triggerEvent(document, "ready");
            isReady = true;
        }
    }

    if(document.readyState === "complete"){
        ready();
    }
    // 对于w3c浏览器,创建一个DOMContentLoaded事件处理程序,触发ready处理程序,然后再删除自身
    if(document.addEventListener){
        contentLoadedHandler = function(){
            document.removeEventListener("DOMContentLoaded",contentLoadedHandler,false);
            ready();
        }
        document.addEventListener("DOMContentLoaded",contentLoadedHandler,false);
    }else if(document.attachEvent){
        contentLoadedHandler = function(){
            // IE内核通过判断document.readyState属性判断DOM已经加载完毕
            if(document.readyState === "complete"){
                document.attachEvent("onreadystatechange",contentLoadedHandler);
                ready();
            }
        }
        document.attachEvent("onreadystatechange",contentLoadedHandler);

        var toplevel = false;
        try{
            toplevel = window.frameElement == null;
        }catch(e){}

        if(document.documentElement.doScroll && toplevel){
            doScrollCheck();
        }
        function doScrollCheck(){
            if(isReady) return;
            try{
                document.documentElement.doScroll("left");
            }catch(error){
                setTimeout(doScrollCheck,1);
                return;
            }
            ready();
        }
    }
})()

总结

说实话,这本书是跳着看的~~~
中间精华部分还是正则表达式部分。
个人觉得介绍JS基础的好好的去啃(舔来舔去,当一个添狗)几本圣经是最好的方式。其他的书籍选择性的去看,这样会有其他角度的理解。
PS:感觉翻译的不太好。一周读完

相关文章

  • 2019-01-24

    读 javascript 忍者秘籍 本文是作者阅读 javascript 忍者秘籍这本书过程中所记录零散知识和学习...

  • JavaScript忍者秘籍

    函数调用 构造器 构造器(constructor)被调用时候会发生: 创建一个新对象 传递给构造器的对象是this...

  • JavaScript忍者秘籍

    JavaScript忍者秘籍是jQuery之父(约翰-莱西格)写的一本关于JavaScript进阶的书籍,而我看的...

  • 忍者级别的JavaScript函数操作

    从名字即可看书,此篇博客总结与《JavaScript忍者秘籍》。对于JavaScript来说,函数为第一类型对象。...

  • JavaScript 忍者秘籍笔记——进入忍者世界

    第一章 进入忍者世界 一个 javascript 库的组成可以分为如下三个方面: javascript 语言的高级...

  • JavaScript -- 挥舞函数

    本文中的内容来自于 《JavaScript 忍者秘籍》。 函数存储 利用以下代码可以完成函数存储功能。 使用场景:...

  • 函数(二)arguments与this

    本文对应《JavaScript忍者秘籍》第4章内容。函数除了显式声明的形参,还有两个隐含的参数:arguments...

  • javascript阅读笔记(2)-《javascript 忍者

    上一个笔记没有写完,因为感觉有些问题还不是太清楚,又翻了下《javascript 忍者秘籍》,前面看了几次,每次都...

  • 函数(一)定义与参数

    本文对应《JavaScript忍者秘籍》第3章内容。 1.函数式编程 函数是第一类对象。 函数和对象 共性: 通过...

  • 函数(三)闭包和作用域

    本文对应《JavaScript忍者秘籍》第5章内容。 1.理解闭包 闭包允许函数访问并操作函数外部的变量,只要变量...

网友评论

      本文标题:JavaScript忍者秘籍

      本文链接:https://www.haomeiwen.com/subject/jgmjqqtx.html