小黄怪

小黄怪 查看完整档案

填写现居城市  |  填写毕业院校  |  填写所在公司/组织 liaojungang.com 编辑
编辑

学生一枚,MERN栈

个人动态

小黄怪 赞了回答 · 2019-10-10

解决在JavaScript的立即执行的具名函数A内修改A的值时到底发生了什么?

先说结论: 第一段代码中 A = 1 试图修改自由变量 A的值,但没有生效!

(function A() {
    console.log(A); // [Function A]
    A = 1;
    console.log(window.A); // undefined
    console.log(A); // [Function A]
}())

这是一个立即执行的函数表达式(Immediately-invoked function expression, IIFE),更特殊的,该函数表达式是一个具名函数表达式(Named function expression, NFE)。

NFE 有两个好玩的特性:

  • 作为函数名的标识符(在这里是 A )只能从函数体内部访问,在函数外部访问不到 (IE9+)。(kangax
    有一篇 博客 详细讨论了 NFE 以及 IE6~8 的 JScript bug,推荐阅读!) ES5 Section
    13
    特别提及了这一点:

The Identifier in a FunctionExpression can be referenced from inside the FunctionExpression's FunctionBody to allow the function to call itself recursively. However, unlike in a FunctionDeclaration, the Identifier in a FunctionExpression cannot be referenced from and does not affect the scope enclosing the FunctionExpression.

  • 绑定为函数名的标识符(在这里是A)不能再绑定为其它值,即该标识符绑定是不可更改的(immutable),所以在 NFE 函数体内对 A 重新赋值是无效的。ES5 Section 13 详细描述了创建 NFE 的机制:

The production FunctionExpression : function Identifier (
FormalParameterListopt ) { FunctionBody }
is evaluated as follows:

  1. Let funcEnv be the result of calling NewDeclarativeEnvironment passing the running execution context’s Lexical Environment as the argument
  2. Let envRec be funcEnv’s environment record.
  3. Call the CreateImmutableBinding concrete method of envRec passing the String value of Identifier as the argument.
  4. Let closure be the result of creating a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt and body specified by FunctionBody. Pass in funcEnv as the Scope. Pass in true as the Strict flag if the FunctionExpression is contained in strict code or if its FunctionBody is strict code.
  5. Call the InitializeImmutableBinding concrete method of envRec passing the String value of Identifier and closure as the arguments.
  6. Return closure.

注意步骤 3 和 5,分别调用了 createImmutableBinding 和 InitializeImmutableBinding 内部方法,创建的是不可更改的绑定

要理解这两个特性,最重要的是搞清楚标识符 A的绑定记录保存在哪里。让我们问自己几个问题:

  1. 标识符 A 与 该 NFE 是什么关系? 两层关系:首先,该 NFE 的 name 属性是 字符串 'A';更重要的是,A是该 NFE 的一个自由变量。在函数体内部,我们引用了 A,但 A 既不是该 NFE 的形参,又不是它的局部变量,那它不是自由变量是什么!解析自由变量,要从函数的 [[scope]] 内部属性所保存的词法环境 (Lexical Environment) 中查找变量的绑定记录。

  2. 标识符 A 保存在全局执行环境(Global Execution Context)的词法环境(Lexical Environment)中吗? 答案是否。如果你仔细看过 ES5 Section 13 这一节,会发现创建 NFE 比创建 匿名函数表达式 (Anonymous Function Expression, AFE) 和 函数声明 (Function Declaration) 的过程要复杂得多:

对于 创建 AFE 和 FD, 步骤是这样:

The production
FunctionDeclaration : function Identifier ( FormalParameterListopt ) { FunctionBody }
is instantiated as follows during Declaration Binding instantiation (10.5):

Return the result of creating a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt, and body specified by FunctionBody. Pass in the VariableEnvironment of the running execution context as the Scope. Pass in true as the Strict flag if the FunctionDeclaration is contained in strict code or if its FunctionBody is strict code.

The production
FunctionExpression : function ( FormalParameterListopt ) { FunctionBody }
is evaluated as follows:

Return the result of creating a new Function object as specified in 13.2 with parameters specified by FormalParameterListopt and body specified by FunctionBody. Pass in the LexicalEnvironment of the running execution context as the Scope. Pass in true as the Strict flag if the FunctionExpression is contained in strict code or if its FunctionBody is strict code.

比创建 NFE 简单多了有木有?那为么子创建 NFE 要搞得那么复杂呢?就是为了实现 NFE 的只能从函数内部访问A,而不能从外部访问这一特性!咋实现的? 创建 NFE 时,创建了一个专门的词法环境用于保存 A 的绑定记录(见上面步骤 1~3)!对于 NFE, 有如下关系:

A.[[scope]] --->  Lexical Environment {'environment record': {A: function ...}, outer: --}----> Lexical Environment of Global Context {'environment record': {...}, outer --}---> null

可见,A 的绑定记录不在全局执行上下文的词法环境中,故不能从外部访问!

关注 17 回答 4

小黄怪 关注了用户 · 2019-03-11

蛋白 @danbai

关注 6

小黄怪 回答了问题 · 2019-03-10

GraphQL怎么上传文件?

如果你使用了Apollo系列框架,则apollo-upload-client是可以实现文件上传的,具体可见此包的readme

关注 6 回答 5

小黄怪 回答了问题 · 2019-03-10

【mongoose】aggregate 如何查询返回下标

第一点,你match的是_id,正常情况下应该是只会查询到一条文档的,怎么会返回多条呢?

假设你的_id是非unique的,你想要给每条文档新增一个index属性,这也是很奇怪的想法,因为如果还有后续的聚合操作的话,文档顺序总是改变的,加index完全没必要;如果没有后续聚合了,那么返回的文档进行一次map操作手动添加即可

关注 2 回答 1

小黄怪 回答了问题 · 2019-03-10

mongoose如何做一对多查询

  1. 你的t.students.push(s);写法是不对的,应该是t.students.push(s._id);
  2. 把1000个学生给老师,那就是循环1000次,然后在进行数据库的存储操作,可以采用异步方式进行避免堵塞

关注 3 回答 2

小黄怪 回答了问题 · 2019-03-10

node mongoose methods 怎么在调用的时候拿不到该方法

你的第三个代码块里写的是user.comparePassword(...),这里的user是你在第二个代码块里导出的mongoose.model,问题就在这了。方法是需要添加给schema而不是model,具体如下:

下面是方法定义:

newSchema.methods.comparePassword_thisIsDoc = function(cb) {
    // 这里的this指代的是`Document`对象
}
newSchema.statics.comparePassword_thisIsModel = function(pwd, cb) {
    // 这里的this指代的是`Model`对象
}

下面是具体使用:

user.find({email: ctx.request.body.email}, (err, res) => {
    const doc = res[0];
    if(doc!==null) {
        // 下面是使用查询到的document判断
        doc.comparePassword_thisIsDoc(cb)
        // 下面是使用model判断
        // user.comparePassword_thisIsModel(pwd, cb);
    }
      return res
}); 

PS: 你上述写的let results = await user.find(...)是无效的写法,正确应该这么写:

// 同步式写法
let results = await user.find(...).exec();
// 回调式写法 [err总位于参数第一位]
user.find(..., function(err, docs) {
    
})

关注 2 回答 1

小黄怪 回答了问题 · 2019-03-10

基于typescript的react项目中Ref的类型确定的一个疑问!

typescript作为类型语言,就是为了可以提供尽可能严格的类型检测,所以办法无非就是你上述提到的两种:要不判断,要不加!

typescript写起来真的会让人感觉很麻烦,这也算是typescript的优点带来的“缺点”吧,但为了高可维护可预测的代码,我们必须选择妥协,不然还是用js写吧。

关注 4 回答 2

小黄怪 回答了问题 · 2019-03-10

node + express 开发中如何进行异常处理

除了使用上面的亲提到的 采用中间件来处理 express路由过程中的error外,为了避免仍有未捕获到的异常导致应用垮掉,可考虑加入以下的全局错误监听:

process.on('uncaughtException', (err) => {
    console.log('这里有个未捕获的同步错误', err);
})
process.on('unhandledRejection', (err) => {
    console.log('这里有个未处理的异步错误', err)
})

关注 3 回答 2

小黄怪 回答了问题 · 2019-03-10

解决react如何修改两个相同自定义组件其中一个的样式

Formlat组件可改写为如下:

import React from 'react';
class Formlat extends React.Component{
    render (){
        return (
            <div style={this.props.style}>
                ...
            </div>
        )
    }
}
export default Formlat;

使用方法:

<Formlat style={{background: 'blue'}} />
<Formlat />

你的代码是使用class的方式,思想是一样的,把传递style变成传递class就ok啦!

关注 1 回答 1

小黄怪 赞了文章 · 2018-10-19

使用Jest测试JavaScript(Mock篇)

在本篇教程中,我们会介绍 Jest 中的三个与 Mock 函数相关的API,分别是jest.fn()jest.spyOn()jest.mock()。使用它们创建Mock函数能够帮助我们更好的测试项目中一些逻辑较复杂的代码,例如测试函数的嵌套调用,回调函数的调用等。

如果你还不知道Jest的基本使用方法,请先阅读: 《使用Jest测试JavaScript (入门篇)》

为什么要使用Mock函数?

在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。

Mock函数提供的以下三种特性,在我们写测试代码时十分有用:

  • 捕获函数调用情况
  • 设置函数返回值
  • 改变函数的内部实现
我们接着使用上篇文章中的目录结构,在test/functions.test.js文件中编写测试代码,src/目录下写被测试代码。

1. jest.fn()

jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。

// functions.test.js

test('测试jest.fn()调用', () => {
  let mockFn = jest.fn();
  let result = mockFn(1, 2, 3);

  // 断言mockFn的执行后返回undefined
  expect(result).toBeUndefined();
  // 断言mockFn被调用
  expect(mockFn).toBeCalled();
  // 断言mockFn被调用了一次
  expect(mockFn).toBeCalledTimes(1);
  // 断言mockFn传入的参数为1, 2, 3
  expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})

jest.fn()所创建的Mock函数还可以设置返回值定义内部实现返回Promise对象

// functions.test.js

test('测试jest.fn()返回固定值', () => {
  let mockFn = jest.fn().mockReturnValue('default');
  // 断言mockFn执行后返回值为default
  expect(mockFn()).toBe('default');
})

test('测试jest.fn()内部实现', () => {
  let mockFn = jest.fn((num1, num2) => {
    return num1 * num2;
  })
  // 断言mockFn执行后返回100
  expect(mockFn(10, 10)).toBe(100);
})

test('测试jest.fn()返回Promise', async () => {
  let mockFn = jest.fn().mockResolvedValue('default');
  let result = await mockFn();
  // 断言mockFn通过await关键字执行后返回值为default
  expect(result).toBe('default');
  // 断言mockFn调用后返回的是Promise对象
  expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]");
})

上面的代码是jest.fn()提供的几个常用的API和断言语句,下面我们在src/fetch.js文件中写一些被测试代码,以更加接近业务的方式来理解Mock函数的实际应用。

被测试代码中依赖了axios这个常用的请求库和JSONPlaceholder这个上篇文章中提到免费的请求接口,请先在shell中执行npm install axios --save安装依赖,。
// fetch.js

import axios from 'axios';

export default {
  async fetchPostsList(callback) {
    return axios.get('https://jsonplaceholder.typicode.com/posts').then(res => {
      return callback(res.data);
    })
  }
}

我们在fetch.js中封装了一个fetchPostsList方法,该方法请求了JSONPlaceholder提供的接口,并通过传入的回调函数返回处理过的返回值。如果我们想测试该接口能够被正常请求,只需要捕获到传入的回调函数能够被正常的调用即可。下面是functions.test.js中的测试的代码。

import fetch from '../src/fetch.js'

test('fetchPostsList中的回调函数应该能够被调用', async () => {
  expect.assertions(1);
  let mockFn = jest.fn();
  await fetch.fetchPostsList(mockFn);

  // 断言mockFn被调用
  expect(mockFn).toBeCalled();
})

2. jest.mock()

fetch.js文件夹中封装的请求方法可能我们在其他模块被调用的时候,并不需要进行实际的请求(请求方法已经通过单侧或需要该方法返回非真实数据)。此时,使用jest.mock()去mock整个模块是十分有必要的。

下面我们在src/fetch.js的同级目录下创建一个src/events.js

// events.js

import fetch from './fetch';

export default {
  async getPostList() {
    return fetch.fetchPostsList(data => {
      console.log('fetchPostsList be called!');
      // do something
    });
  }
}

functions.test.js中的测试代码如下:

// functions.test.js

import events from '../src/events';
import fetch from '../src/fetch';

jest.mock('../src/fetch.js');

test('mock 整个 fetch.js模块', async () => {
  expect.assertions(2);
  await events.getPostList();
  expect(fetch.fetchPostsList).toHaveBeenCalled();
  expect(fetch.fetchPostsList).toHaveBeenCalledTimes(1);
});

在测试代码中我们使用了jest.mock('../src/fetch.js')去mock整个fetch.js模块。如果注释掉这行代码,执行测试脚本时会出现以下报错信息

从这个报错中,我们可以总结出一个重要的结论:

在jest中如果想捕获函数的调用情况,则该函数必须被mock或者spy!

3. jest.spyOn()

jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。实际上,jest.spyOn()jest.fn()的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数。

上图是之前jest.mock()的示例代码中的正确执行结果的截图,从shell脚本中可以看到console.log('fetchPostsList be called!');这行代码并没有在shell中被打印,这是因为通过jest.mock()后,模块内的方法是不会被jest所实际执行的。这时我们就需要使用jest.spyOn()

// functions.test.js

import events from '../src/events';
import fetch from '../src/fetch';

test('使用jest.spyOn()监控fetch.fetchPostsList被正常调用', async() => {
  expect.assertions(2);
  const spyFn = jest.spyOn(fetch, 'fetchPostsList');
  await events.getPostList();
  expect(spyFn).toHaveBeenCalled();
  expect(spyFn).toHaveBeenCalledTimes(1);
})

执行npm run test后,可以看到shell中的打印信息,说明通过jest.spyOn()fetchPostsList被正常的执行了。

4. 总结

这篇文章中我们介绍了jest.fn(),jest.mock()jest.spyOn()来创建mock函数,通过mock函数我们可以通过以下三个特性去更好的编写我们的测试代码:

  • 捕获函数调用情况
  • 设置函数返回值
  • 改变函数的内部实现

在实际项目的单元测试中,jest.fn()常被用来进行某些有回调函数的测试;jest.mock()可以mock整个模块中的方法,当某个模块已经被单元测试100%覆盖时,使用jest.mock()去mock该模块,节约测试时间和测试的冗余度是十分必要;当需要测试某些必须被完整执行的方法时,常常需要使用jest.spyOn()。这些都需要开发者根据实际的业务代码灵活选择。

查看原文

赞 36 收藏 19 评论 3

认证与成就

  • 获得 16 次点赞
  • 获得 27 枚徽章 获得 0 枚金徽章, 获得 5 枚银徽章, 获得 22 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2017-02-26
个人主页被 1.1k 人浏览