Giter Site home page Giter Site logo

junqu.github.io's Introduction

Hi there 👋

Qu's github stats

junqu.github.io's People

Contributors

junqu avatar

Stargazers

 avatar

Watchers

 avatar

junqu.github.io's Issues

看懂this的指向

this是指函数运行所在环境。函数(function)在运行时,会有两个隐含的参数this和arguments,arguments是当前传入的参数,而this就是当前运行环境,由于各自的特殊且不可被替代的功能,所以很多时候是不可避免的使用。而我对this之前也是恐惧的,常出现非预期的结果让我困惑,我决定暂时先梳理一下this,虽然不能覆盖全面,但是也需要初步了解如何使用this。

首先是考虑全局

全局往往时最容易被忽略的,当this在全局时,也有些不同。在浏览器中,this指向时window。

console.log(this) // window
var a = 1           // 这里用ES6的let、const会undefined
console.log(this.a) // 1 

但是在node中,常常说的是this指向global,但由于全部模块化,直接使用this其实并不是global,而是exports,至于究竟是什么情况,看到一个讨论 ,也可以在官方文档中能找到结果。当然,node中this指向global也是可以验证的。

console.log(this===exports) // true
console.log(this) // {}

function f() {
    console.log(this===global)
}
f() //true 非严格模式

所以node的this可能与浏览器的一些情况有不同,这里我由于初学,也并不懂node,暂时就不考虑node的情况。

通用情况

this是函数的一个参数,是函数运行时所在的对象(环境)。对此,目前我浅显的有这么一个想法,假如存在100个人的空间,每个人需要向其他人介绍自己的名字,那么可以用代码这么表示:

tom = {
    name: 'tom',
    say: console.log('I am tom')
}
tom.say

那么你需要为每个人都指定好台词(代码),为100个人准备还挺轻松。但是当新加入几百人还不确定情况时,你就必须做出改变,确保介绍人能在不同人演说同一个台词却能有属于自己的不同的结果,那么这个时候就需要引入this,来指定每个人的名字不同情况,于是代码就可以被写成这个样子:

function say(){
    console.log('I am '+this.name)
}
tom = {
    name: 'tom',
    say: say
}
jame = {
    name: 'jame',
    say: say
}
tom.say() // I am tom
jame.say() // I am jame

这样每个人只要拿个相同的台词(say()),就能说出不同的话,来再多的人也不怕麻烦和遗漏了。通过这个一个简单例子可以发现一个识别this,就是谁说话,谁就是this,即函数的调用者,例如tom.say() 就是tom 调用say 方法,注意这里是.前一个就是调用者,像kocool.co.co.say()那么调用者应该是kocool.co.co。总结一下就是,看方法左边部分,是reference(引用类型),this就是这个reference,否则不是reference就是全局对象,而在严格模式中,指向全局对象可能报错。

4f237

这个也就是被调用的方法中this,这样确实可以解决大部分调用时的this问题。

迷惑性的写法,虽然不实用,但可以了解this,

obj = {
    foo : function () {
        console.log(this)
    }
}
(obj.foo = obj.foo)()

虽然它非常的绕,但是其实就是obj.foo中函数被赋值后,在全局中被调用了,等价下面的代码:

var f = obj.foo
f() // window

因为这里的函数为什么没有是在对象中调用,因为函数的是引用类型,对象只是指向了函数的地址,阮老师有生动的图文解释所以,这个时候的函数被重新在全局环境中调用了,指向的自然就是全局对象(浏览器中是window)。

分类情况讨论this的指向

这些情况看起来很特殊,但是看this永远还是看它的执行环境。

立即执行函数中(IIFE)的this

let obj = {
    foo: function () {
        console.log(this)
    }()
}
obj.foo

这里还是得看函数在哪里被调用的,虽然是立即被调用了,但是前面阮老师已经解释过,函数并不属于对象,对象只是指向函数的地址,那么函数在哪呢,当然是内存中,立即执行就是被全局(window)调用并返回结果,然后obj.foo指向函数的返回结果,所以函数的this指向的就是全局(this)。当然,这里我并没有深入了解过IIFE,但是想着函数不属于对象中,才推出这样的结果,在浏览器中验证与我想法恰好一致。

JS库中高阶函数的this

在数组的map,forEach,map等高阶函数,传入的是函数,并没有绑定this,所以它指向的是window。

阮老师代码挺好的,我学习一下:

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    });
  }
}

o.f()
// undefined a1
// undefined a2

这里,因为forEach里面的function没有绑定this,执行环境就是全局,所以this.v是undefined。当然高阶函数都有第二个参数,用于绑定this,上面的代码改一下就能达到预期,这里传入的this就是this.p的this,就是o

var o = {
  v: 'hello',
  p: [ 'a1', 'a2' ],
  f: function f() {
    this.p.forEach(function (item) {
      console.log(this.v + ' ' + item);
    }, this);
  }
}
o.f()
// hello a1
// hello a2

箭头函数的this

箭头函数不太好直接调用和运行,我使用了立即执行

let obj = {
    name : 'tom',
    say1 : ()=>{
             console.log(this.name)
        },
    say2: function () {
        (()=>{
            console.log(this.name)
        })()
    }
}
obj.say1() // undefined
obj.say2() // tom

同时又测试了另一种形式:

let a = 1
let test = () => {
    console.log(this)
    console.log(this.a)
}
let oj = {
    a:2,
    test
}
oj.test() 
// window 
// undefined

得出规律是this是看它的外层作用域,即它只是类似if中的语句块一样,在查看箭头函数的this一定是往他的外层作用域看,把箭头函数里面的语句放到它的函数体的外层运行。例如在say1中相当于

say1:console.log(this.name)

这里this肯定就是window,因为this没有被绑定到任何地方。然后就是obj.say2()为什么是这样,这时候箭头函数的作用域是外层的function的作用域块,因为function声明的函数this看他调用者,这一句obj.say2()显然调用者是obj,也就是this。test只是另一种方式把箭头函数调用了,看的方式还是看外层作用域,显然没有像function限制this或者call去绑定this,那一定就是全局对象(window)。总结还是箭头函数看他的外层作用域,外层作用域的this就是它的this。

回调函数的this

常见回调函数setTimeout为例,普通的例如:

var a = 1
function fn() {
    console.log(this.a)
}
setTimeout(fn, 2000) // 1

很明显和高阶函数一样因为传入的是函数,在没有绑定或者被调用情况下,肯定就是windowsetTimeout特殊点在于严格模式也是指向window而不是undefined,由于setTimeout本身使用call之类的绑定this会失败,MDN专门有解决方案。

new操作的this

就先不讨论,以后慢慢补上,记住的是构造函数的this是绑定在对象上的(这一点倒是和我之前学Java很类似了)。

call,apply,bind

应用

自己实现

典型的面试题

有些取巧迷惑的面试题分享一下

第一道就是大菜,我印象深刻

var obj = {
    leng: 2,
    method1: function (fn) {
        fn();
        fn.call(this);
        arguments[0]()
    },
    method2:function () {
        document.addEventListener('click',evt => this.method3(evt.type),false)
    },
    method3: function (type) {
        console.log(type+':'+this.leng)
    }
}
问:
obj.method1(fn)
obj.method2(fn)
会输出什么?为什么

非常迷惑的题目,专治模棱两可。

还有类似的。没有上面的好

var obj2 = {
    birth: 1990,
    getAge: function () {
        var b = this.birth
        var fn = function () {
            return new Date().getFullYear() - this.birth
        }.bind(this)
        return fn()
    }
}
问:
console.log(obj2.getAge()) 

慢慢来,发现this真要深究感觉有点深不可测。还有好多的场景没有去了解的他们的this,例如包装类型。

拖拖拉拉写了一天也才2000字,由于嫌麻烦好多测试的图就不放了,上面代码基本都在几个浏览器和node环境测试过。
lodash自己实现了150+但是感觉没时间总结,总结对我而言过于耗时(语文太差),先写练习,练的多才会,为什么人一定要睡8小时,,,最近搞得自己身心俱疲的

参考链接:

JavaScript 的 this 原理

根治JavaScript中的this-ECMAScript规范解读

彻底弄清 this call apply bind 以及原生实现

如何判断NaN

1 介绍

NaN的定义是:

The global NaN property is a value representing Not-A-Number.

表面说自己不是number,但是本意是一个number类型(真讨厌~~):

typeof NaN // number

它的一些属性:

NaN 属性的属性特性:
writable false
enumerable false
configurable false

还有一个重要特性就是它与任何东西都不相等包括他自己,即NaN != NaN
其他的,NaN0false""nullundefined都属于Falsy值。

一般情况也较少用到,但是由于它独特的性质会在遇到它时非常棘手。比如在数组去重的时候,NaN的出现常常会给你带来不必要的麻烦。所以,当你需要处理NaN的时候,你迫切的需要一个判断NaN的方法,由于NaN的特殊性,原生JS库就有判断的方法。

我作为一个初学者,目前其实很少遇到处理NaN,刷Lodash时,看着自己实现的NaN觉得很多不足,找了一些方法,算是有点收获,于是笔记记一下以免再次掉坑里。

2 实现

在我网上找解决方法的时候发现很多判断NaN的方法很可能都是错的,基本上包含了所有的博客类或者社区类文章,看了整整一天没发现好文章。不是我错了,是全世界错了?还是我错了?带着疑问我步步推进。

isNaN()

这是大家都避免过的坑了,也是很久之前的面试题。听到NaN,第一个大坑就是这个。isNaN()作为官方提供的全局方法应该是很可靠的,开始我都是用这个,官方的人比我牛逼多了,想的肯定比我多,稍微试一下:

isNaN(123)   // false
isNaN('123') // false
isNaN(NaN)   // true

官方的函数言简意赅,好像还不错,但是随着我测试多了起来发现不对:

isNaN('a') // true
isNaN(function(){return 1}) // true

为什么是这样呢?经过我的查找,因为它传入的值,首先会通过Number()函数转化为Number类型再进行NaN的判断(Confusing special-case behavior),至于为什么转化官方也有说明(我没看)。所以导致传入的值是不可以被转化为数字的情况时,无法判断他是不是NaN。即类似'a123',经过Number()函数转化后返回结果是NaN,再判断是就NaN了。所以不能用这个函数判断某个值是NaN

Number.isNaN()

因为转化Number的关系导致isNaN变成大坑,于是我先判断它是不是Number类型不就可以了。网上很多文章在发现isNaN()是个大坑后,基本都是采取这种解决方式,写出的结果大概如下:

function isNaN(val) {
    return isNumber(val) && isNaN(val)
}

通过isNumber确定它是Number类型,然后再进行判断。官方再发现isNaN()的问题后也进行了修补,就是Number.isNaN() 实现方式基本一样:

Number.isNaN = Number.isNaN || function(value) {
    return typeof value === "number" && isNaN(value);
}

这次基本上就修补了那些isNaN()的怪异行为,测试一下确实如此:

Number.isNaN("NaN");      // false
Number.isNaN(undefined);  // false
Number.isNaN("a");        // false

开心,一切都是那么的完美了!

在推出Number.isNaN()后,仿佛一切都得到了解决。在我刷lodash前我也是这么认为的,官方的修补应该是不会出错,那么判断NaN是不是就是Number.isNaN就可以搞定了呢?

_.isNaN()

Lodash总能让我得到许多惊喜。Lodash的_.isNaN函数有个官方示例非常有趣,如下:

_.isNaN(new Number(NaN));
// => true

好像没什么特别,于是我立即用Number.isNaN()对比一下,得出了一些结果:

Number.isNaN(new Number(NaN)) // false
Number.isNaN(new Number(123)) // false

明显的不同,不由的看了Lodash的实现,是这样的:

    function isNaN(value) {
      // An `NaN` primitive is the only value that is not equal to itself.
      // Perform the `toStringTag` check first to avoid errors with some
      // ActiveX objects in IE.
      return isNumber(value) && value != +value;
    }

函数中间夹杂着注释,在Lodash我目前只见过这一个,显然它描述着为什么他们会采取这种方式来验证NaN。首先还是判断它的类型是Number,接着进行NaN的判断,NaN判断的方式还是用它自己不等于自己的特性;==的目的在于这个判断需要进行隐式转换;至于为什么后面有个+号,+作用是为了转换,例如+'123'会把字符串自动转化为123,但是由于对==的不熟悉,找了半天都头痛,对于==的转换还是推荐一个回答 ,就不要强求了,我只认=== 。所以在某种意义上好像解决了目前所有的问题。

但是我还有点点想法,关于这个判断,因为NaN其中一个特性就是不会等于自己,但是new Number(NaN)是可以等于自己的,其实不难理解,因为这时候他成为了对象,一个引用类型,指向同一个地址肯定是相等的,这个时候的NaN像是Number的一个属性,这时候的NaN是不是NaN呢?我觉得还是一个 疑问号,按照官方理解应该不是,但是常用库lodash修复了就意味着认可他是。

总结

NaN真是一个特殊的值,通过它反映JS存在的问题的写照。JS存在很多坑点,但是能有这么强大的生命力。在其他语言这种类型判断还要想半天感觉很奇怪,也是JS的“特性”吧。

假如你可以保证只使用基本类型时,可以利用不等于自身的方法:

function isNaN (val) {
    return val !== val
}

假如你需要更健壮的方法,或者说你不知道什么类型的情况下,绝大部分情况下都推荐使用系统自带的Number.isNaN() 。当然,也可以自己实现一个类似的:

function isNaN (value) {
    return typeof value === "number" && isNaN(value)
}

假如你使用了包装类型,或者想判断包装类型的NaN那么需要使用Lodash库,或者自己写一个类似的:

function isNaN (value) {
    return isNumber(value) && value != +value
}

这里的isNumber推荐使用原型的方法进行判断。

得到教训:

1、通常情况使用原生库自带的Number.isNaN

2、永远不要使用基本包装类型感觉包装类型得不偿失,对于 new Number()new Boolean()new String()这种创建包装对象,能避免坚决避免。

3、永远不要直接使用isNaN方法,起码需要类型判断。

最后好像终于告一段落了,这一个方法看到大家实现的奇怪,于是自己扎进去看了一下,醒来发现自己已经落后了?

周围的人很多时候都劝我不要搞这些牛角尖问题。但是它就像一小个山顶,很少人去,去了的人告诉我没什么意思,其他没去过的人看来太过于浪费时间和精力,往往这时候去爬着试试会感觉很有趣,中间过程会让人迷茫亦或憧憬于山顶的景象,但是很可能吃力的爬到山顶却发现那里什么都没有,一切也没啥意思,仿佛失去了意义,但是我不会后悔去爬任何一座小山顶,因为那中间的过程才是我想要的东西。

参考链接:

你可能不知道的 NaN 以及 underscore 1.8.3 _.isNaN 的一个 BUG
Javascript 中 == 和 === 区别是什么
Number.isNaN()

浅尝辄止数据结构--Stack

前言

栈(stack)是一种基础的数据结构,它在计算机的用途十分广泛(例如在计算机内存中),但是一般很难直接接触到。目前水平也未能发现直接使用栈工作的,所以现在是学习数据结构,我需要利用JS中的数组模拟一个栈出来。

实现栈

栈的特点是先进后出,即先进来的元素在栈底,后来的元素在栈顶,出栈从栈顶开始。根据它的特性,通过js自带的语法很轻松就能实现一个基础的栈结构,代码如下:

const stack = function () {
    // 存放栈内的元素
    let items = [];
    // 向栈添加元素
    this.push = (element) => {
        items.push(element);
    }
    // 从栈移除元素
    this.pop  = () => {
        return items.pop();
    }
    // 查看栈顶元素(最后入栈的元素)
    this.peek = () => {
        return items[items.length -1];
    }
    // 判断栈内是否有元素
    this.isEmpty = () => {
        return items.length === 0;
    }
    // 清空栈
    this.clear = () => {
        items = [];
    }
    // 查看栈内元素数量
    this.size = () => {
        return items.length;
    }
    // 打印栈内元素
    this.print = () => {
        console.log(items.toString());
    }
};

实现的非常简陋,我才刚学JS不久,对JS也不熟,甚至写这个时都处于不懂this的指向的,不过经过简单的几个测试发现是可以正常的操作的,那暂时就这样了。( ̄▽ ̄)"

很明显,JS数组就很类似栈了,所以一般实际使用都是数组,把它看作一个栈就好。

栈的应用

当然这里的应用不是实际的,仅仅指的是练习题的应用。之前我一直体会不到栈的特性,想着有了数组为什么还要有栈,做完几个题目,就能体会栈在完成某些事情时,能让人十分顺手。

Basic Calculator

Basic Calculator 这道题目是为了找找关于栈特点的题目,一开始没注意难度,只是觉得题目很有特点,能有实际应用的感觉,而且我以后会实现一个计算器,那就选择这道题来了解栈的应用。这题我做不了,甚至在看了相关的解答视频我仍然处于懵懂状态,于是我只能调试一下别人的答案,把别人答案一步一步的调试,自己才慢慢懂得为什么要使用栈。不得不说,栈在用于简单数据的缓存非常有效。

题目就请去网上查看了,这里列一下用JS栈的解法,这里我只是把别人的代码稍加修改的。

/**
 * @param {string} s
 * @return {number}
 */
var calculate = function(s) {
    let str = s.trim();// 去除多余空格
    let stack = [];// 创建栈 
    let result = 0;// 存放结果
    let symbol = 1;// 符号位,判断是+就为1,是—就是-1
    let digit = 0;// 用于取出每个数字
    for (let i = 0; i < str.length; i++) {
        if (str.charCodeAt(i) >= 48 && str.charCodeAt(i) <= 57) {
            digit = digit * 10 + Number(str[i]); // 取出每个完整的数字
            if (i === str.length - 1) {
                result += symbol * digit; // 假如最后是数字,这个循环直接就停了,需要处理一下
            }
        }else if (str[i] !== ' ') {
            result += symbol * digit;// 当没有括号,就是直接计算
            digit = 0; // 重置数字
            switch (str[i]) {
                case '+':
                    symbol = 1;
                    break;
                case '-':
                    symbol = -1;
                    break;
                case '(':
                    stack.push(result);// 先保存计算结果
                    stack.push(symbol);// 后保存符号位
                    symbol = 1;
                    result = 0;
                    break;
                case ')':
                    result = stack.pop() * result + stack.pop(); // 第一个pop是取出括号前面的符号位,第二个pop是取出符号前的数字,进行相加。
                    symbol = 1;
                    break;
                default:
                    break;
            }
        }
    }
    return result;
};

用栈的地方也不多,假如没有括号,那么代码就不需要栈了。这个解题思路在于括号这个很有特点的结构,因为题目表明每次输入都是有效的,而括号都是成对的并且不同括号不能相间隔({[}])出现,所以栈结构非常适合。这个时候,把不含括号的计算结果存入栈,在'('把结果入栈,在')'取出结果(出栈)。所以代码前半部分都是在取数字、符号和括号。关键部分就是在利用栈缓存:

case '(':
stack.push(result);// 先保存计算结果
stack.push(symbol);// 后保存符号位
symbol = 1;
result = 0;
break;

这是同时两个元素入栈,一定要注意顺序,因为出栈的时候也是同时取两个元素。这个时候保存的是'('前面的数字计算结果和符号位。然后就是出栈:

case ')':
result = stack.pop() * result + stack.pop(); 
symbol = 1;
break;

出栈的顺序非常重要。这里我们在遇到')'应该已经计算过一次结果。即我们在栈内保存的都是'('前的计算结果和符号,但是未保存'('和')'中的数据,这个刚好能形成一个流程。于是,先取出符号位相乘得出需要进行的是加法还是减法,然后与前面的结果进行计算。由于括号是成对的,栈内的元素必然会被取完。假如括号外还有元素,循环走完就能得到结果。

学完这道难题,感觉还是不够,于是自己又独立解决了Decode String 两道题目可以说是非常类似,弄清楚栈的使用,以及Basic Calculator题目的特点,在Decode String采用相同的策略,取出字符串和数字,在'['入栈,在']'出栈,注意顺序,非常轻松就能解决。但是总觉得栈在这样的题目,过于特殊,有没有能用栈解决的非常舒畅,或者说与栈关系大一点的题目,于是我想把我之前没有解决的一道题目解决一下。

Next Greater Element II

Next Greater Element I 我是通过map缓存解决的,当时就觉得很浪费空间,而且也发解决II。经过几次思考我还是未能解决Next Greater Element II 看了一下论坛里解答,发现他用栈实现的十分有意思,于是还是贴出来收藏一下想法。通过这次栈的使用,才见识到别人使用的栈了,把栈的特点基本都用上了,而且让人印象深刻

var nextGreaterElements = function(nums) {
    let stack = [] // 栈空间
    let result = [] // 存放编号

    // 第一遍遍历,找到每个数比他大(除最后一个数)的数,找不到就把它的索引号存放在栈空间
    // 在栈空间内,由于先比较再存入栈内,所以必然是栈底的数最大,栈顶的数最小
    // 为什么是while而不是if因为最大的数可能不止一个,假如为小于等于就最大数可能要另外处理
    // 采用小于则需要while进行不断的判断,避免相同元素的问题
    for(let i = 0; i < nums.length; i++) {
        while(stack.length && nums[stack[stack.length - 1]] < nums[i]) { //这里栈的方法应该是peek
            result[stack.pop()] = nums[i] // 得到比大数,同时在栈空间移除已经得到的数
        }
        stack.push(i) // 对每一个数都进行比较
    }
    // 第二遍遍历,只要从头开始循环,找出比栈内元素大的就行。
    // 由于栈顶元素最小,所以只需要一次for就能解决问题
    // while还是解决重复元素的情况
    for(let i = 0; i < nums.length; i++) {
        while (stack.length && nums[stack[stack.length - 1]] < nums[i]) {
            result[stack.pop()] = nums[i]
        }
    }
    //剩下的必然是栈底的元素,也就是最大的数字
    while (stack.length) {
        result[stack.pop()] = -1
    }
    return result
};

看起来有两个循环但是时间复杂度还是O(n)。

以数组 [4,2,8,3,8,4,3,2,3] (称为a) 为例,了解一下这个的思路。创建result数组,存放对应a数组下标的后一个出现的比他大的数,即存放比相应位置大的数。栈stack存放的是大数的下标,因为下标具有唯一性,而元素有很多个。在第一遍for执行完后,result存放的结果就是找出那些数组后边存在比它大的数,于是得到 :

result:[8,8,0,8,0,0,0,3,0] //空部分暂时填充0以便理解
stack: [2,4,5,6,8] // 存放没找到大数的下标号

于是再次循环,这时候只需要比较stack里面存放的元素就行了。再次进行循环比较。由于栈底的元素最大,于是在比较时,栈顶元素不是最大值将会被取走,于是结果如下:

result:[8,8,0,8,0,4,3,4]
stack: [2,4]

stack里面存放的都是最大值的索引,再用一个循环转换就完成了。

这个解法,对于栈理解让我敬佩。它几乎用了clear的所有方法,而且实现的非常巧妙。利用栈的特点,缓存大的数字的索引。学完这题才有点真感觉是如何使用栈解决实际的问题,当然我是在浅尝数据结构,毕竟JS应当运用的是前端,很少接触底层操作了。

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.