hawtim.github.io icon indicating copy to clipboard operation
hawtim.github.io copied to clipboard

为什么往 call 和 apply 的 this 传入 null ?

Open hawtim opened this issue 5 years ago • 1 comments

笔者最近在学习 《JavaScript设计模式与开发实践》。 在 5.6 节的代码中,是一段表单验证的代码,如下,读者可以新开一个 tab在控制台运行一下的代码。

document.write(`<html>
<body>
  <form action="http://example.com/register" id="registerForm" method="post">
    <label htmlFor="">请输入用户名<input type="text" name="userName"/></label>
    <label htmlFor="">请输入密码<input type="text" name="password"/></label>
    <label htmlFor="">请输入手机号码<input type="text" name="phoneNumber"/></label>
    <button>提交</button>
  </form>
</body>
</html>`)
// 在一个Web项目中,注册、登录、修改用户信息等功能的实现都离不开提交表单

// 表单验证可以避免不合法的数据带来不必要的网络开销

var strategies = {
  isNonEmpty: function(value, errorMsg) {
    if (value === '') {
      return errorMsg
    }
  },
  minLength: function(value, length, errorMsg) {
    if (value.length < length) {
      return errorMsg
    }
  },
  isMobile: function(value, errorMsg) {
    if (!/(^1[3|5|7|8|9][0-9]{9}$)/.test(value)) {
      return errorMsg
    }
  }
}

// 接下来实现 validator 类

var validateFunc = function() {
  var validator = new Validator()
  // registerForm 是 dom,做为表单的项,可以通过 registerForm.userName 取得 userName 输入框的 dom 引用
  validator.add(registerForm.userName, 'isNonEmpty', '用户名不能为空')
  validator.add(registerForm.password, 'minLength:6', '用户名不能为空')
  validator.add(registerForm.phoneNumber, 'isMobile', '用户名不能为空')
  var errorMsg = validator.start()
  return errorMsg
}

var registerForm = document.getElementById('registerForm')

registerForm.onsubmit = function() {
  var errorMsg = validateFunc()
  if (errorMsg) {
    console.log(errorMsg)
    return false
  }
  return false
}

var Validator = function() {
  this.cache = []
}

Validator.prototype.add = function(dom, rule, errorMsg) {
  var ary = rule.split(':')
  // 添加到缓存规则中
  this.cache.push(function() {
    var strategy = ary.shift()
    ary.unshift(dom.value)
    ary.push(errorMsg)
    return strategies[strategy].apply(dom, ary)
  })
}

Validator.prototype.start = function() {
  for (var i = 0, validatorFunc; validatorFunc = this.cache[i++];) {
    var msg = validatorFunc()
    if (msg) { // 如果有确切的返回值,说明校验没有通过
      return msg
    }
  }
}

运行效果如图 image

我们注意到代码中 Validator 的 add 方法下

this.cache.push(function() {
    var strategy = ary.shift()
    ary.unshift(dom.value)
    ary.push(errorMsg)
    return strategies[strategy].apply(dom, ary)
  })

最后一行 apply 传入了 dom,我之前一直不解,这里传一个 dom 的引用和传 null 有什么差别? 运行效果不都是一样吗?读者可以试一下,控制台输出的都是”用户名不能为空“ 那它们有什么差别呢?什么时候有差别呢?带着这两个疑问,我们在 strategies 下的 isNonEmpty 下添加一行打印。

有什么差别?

var strategies = {
  isNonEmpty: function(value, errorMsg) {
    console.log(this) // 打印下 this 的输出
    if (value === '') {
      return errorMsg
    }
  },
 // ...
}

我们重新运行,能发现,apply null 的话,在浏览器打印出来的值是 Window。 image

而如果是 apply dom 的话,this 指向的就是对应表单的输入框。 image

那么我们可能还有疑问,为什么传入的是 null,返回回来 this 的指向却是 window 对象呢?这里我们引用一下规范

image

可以看到规范里对 this 这个参数说明了,在非严格模式下,null 或者 undefined 都会变成指向全局对象(浏览器 Window,nodejs 下就是 global),基本类型值则会被包装成对象的形式。

所以我们试试 return strategies[strategy].apply('', ary) 传入字符串,会有什么效果。此时 this 指向的是

image

什么时候有差别?

看了上面 apply 的 this 指向了 null,dom,'' 代码都是一样的输出,什么时候会不一样呢? 查看下面的代码改动

var strategies = {
  isNonEmpty: function(value, errorMsg) {
    console.log(this.value) // 注意这里,打印 dom 的 value 属性值,如果指定错了 this,那么这里无疑不能拿到正确的值。
    if (value === '') {
      return errorMsg
    }
  },
 // ...
}

当我们要在 strategies 下的验证规则中获取 dom 的引用并访问数据时,指定正确的 this 就很关键了。这里我隐约感觉到,为什么 null 和 undefined 会变成指向全局对象,因为 this 变成了 window 了 this.value 也只是 undefined 而不会直接报错。至于是不是这样,等我翻了源码来补充一下哈哈。

结论

当你正在调用 .apply 方法,并且需要访问实例数据,则必须传递适合的对象作为第一个参数,以便该方法有权使用此指针来按预期方式执行工作。

Refer: https://stackoverflow.com/a/33640254

hawtim avatar Aug 21 '20 08:08 hawtim

在node 源码中找到这句,node-12.13.0/deps/v8/ChangeLog: Made optimized Function.prototype.apply safe for non-JSObject first arguments (issue 1128). Google得到: https://chromium.googlesource.com/external/v8/3.7/+/5c0561c62d19036d1d4caa460bff7e55448bcb5a/ChangeLog#1011

历史久远,找不到改动内容,但从注释上来看,针对非对象类型的 this 确实做了一层安全防范。

hawtim avatar Aug 21 '20 09:08 hawtim