iwtofly.github.io's People
iwtofly.github.io's Issues
JS this解析
作用域分为两种: 1. 词法作用域,2. 动态作用域。javascript遵循词法作用域,闭包等概念也是由词法作用域衍生而来。而this是js中一个特别的关键字,被自动定义在所有函数的作用域中。
动态作用域其实是this的表亲,this机制也没有想象中那么复杂,在缺乏清楚认识的情况下,this的结果才会会有些出乎意料。
误区一
function foo(num) {
console.log('foo: ' + num);
this.count++;
// console.log(this.count) // NaN
}
foo.count = 0;
var i = 0;
for (i = 0; i < 10; i++) {
if (i > 5) {
// 如果调用时只用foo(i),最后输出count为0
foo(i);
// foo.call(foo, i);
}
}
console.log(foo.count);
原意为使用count记录foo的调用次数,但是当前的结果却是:
foo: 6
foo: 7
foo: 8
foo: 9
0 // ???
当在foo中打印this.count时,发现count为NaN。而且调试发现,这是一个全局变量。
为什么这是全局变量?为什么这是NaN?
解决办法
- 循环中使用foo.call(foo, i);,确保this指向函数对象foo本身
- 使用 foo.count++;回归词法作用域
另一个误区
function foo() {
var a = 2;
this.bar();
}
function bar() {
console.log(this.a);
}
foo();
在这里利用了this来隐式的引用函数的词法作用域。试图通过this.bar()引用bar函数(这里能调用成功也只是个意外——this指向window),还试图通过this联通foo和bar的作用域,从而使bar可以访问a,然而失败了。
this解析
学习this,需要明确每个函数的调用栈,即调用位置。调用位置和调用方式决定了this的绑定对象。this有以下几种绑定规则:
1. 默认绑定-独立函数调用
function foo() {
console.log(this.a);
}
var a = 2;
foo(); // 2
这里foo的调用未使用任何修饰,因此被进行了默认绑定。非严格模式下,this.a被解析为全局变量a。(严格模式下会报错TypeError)
2. 隐式绑定
调用位置是否有上下文对象,即foo函数式通过哪个对象来调用的。这种情况下,绑定的对象必须包含一个指向函数的属性。
function foo() {
console.log(this.a);
}
var obj1 = {
a: 2,
foo: foo
}
obj1.foo(); // 2
这种绑定只与属性引用链的上一层或者最后一层有关,如下:
function foo() {
console.log(this.a);
}
var obj2 = {
a: 22,
foo: foo
}
var obj1 = {
a: 2,
obj2: obj2
}
obj1.obj2.foo(); // 22
隐式丢失
隐式绑定的函数会丢失绑定对象,转而遵循默认绑定,从而把this绑定到全局对象或者undefined上(非严格模式)。
function foo() {
console.log(this.a);
}
var obj = {
a: 2,
foo: foo
}
var bar = obj.foo;
var a = 'global , oops';
bar(); // 'global , oops'
bar指向了foo,bar的调用不带任何修饰。因此抛出结果与默认绑定一致。此外,若作为参数传入函数也会发生同样的事情,因为函数的参数处理js引擎默认进行了LHS查询赋值,获取到的也是foo函数的引用:
function foo() {
console.log(this.a);
}
function doFoo(fn) {
fn();
}
var obj = {
a: 2,
foo: foo
}
var a = 'global , oops';
doFoo(obj.foo); // 'global , oops'
同理,回调函数中丢失this绑定,也是最常见的。我们通常采取的解决办法是:
- 将setTimeout或者请求、监听中的回调函数进行bind(this),绑定当前作用域的this(硬绑定);
- 或者采取别名保存self=this,借助词法作用域进行访问;
- 使用ES6的箭头函数,它没有自己的 this 、直接继承外部作用域的this;
3. 显式绑定
显式绑定大多数情况下是硬绑定,还有一些第三方库或者框架提供的用于绑定的API。这里只分析硬绑定相关。
其实硬绑定就是通过我们同通常接触到的call、apply、bind
在第一个误区例子中,我们可以使用foo.call(foo,i)就是使用了硬绑定,来解决默认绑定带来的问题。
应用场景1: 创建包裹函数,负责接收参数并返回值。
function foo(something) {
console.log(this.a, somthing);
return this.a + something;
}
var obj = {
a: 2
};
var bar = function() {
return foo.apply(obj, arguments);
}
var b = bar(3); // 2,3
console.log(b); // 5
应用场景2: 创建复用的辅助函数
function foo(something) {
console.log(this.a, somthing);
return this.a + something;
}
function bind(fn, obj) {
return function() {
return fn.apply(obj, arguments);
}
}
var obj = {a: 2};
var bar = bind(foo, obj);
var b = bar(3); // 5
由于硬绑定的使用场景较多,因此ES5提供了Function.prototype.bind函数,它会返回一个硬绑定的新函数,将你指定的参数设置为this上下文,并调用原始函数。
bind()函数的功能之一就是可以把除了第一个参数之外的参数都传递给下层的函数,这种技术称为**“部分应用”,是“柯里化”**的一种。
4. new绑定
在其他语言中,‘构造函数’是类中的特殊方法。使用new初始化时会调用类中的构造函数。但是js中的new机制其实和其他语言不同。
javascript中构造函数只是一些使用new操作符时被调用的函数。并不属于某个类,也不会实例化一个类。
使用new来调用函数,会执行:
- 1 创建一个全新对象
- 2 该对象会被执行[[prototype]]连接
- 3 新对象会绑定到函数调用时的this
- 4 如果函数中没有返回其他对象,new表达式中的函数调用会自动返回这个新对象
eg:
function foo(a) {
this.a = a;
}
var bar = new foo(); // 1.创建bar需要的对象,2. prototype链接,3. this绑定到bar对象 4. 返回新的bar对象
console.log(bar.a);
优先级
默认绑定 < 隐式绑定 < 显式绑定/new绑定
判断this方法
- 函数是否在new中调用——this是创建的对象
- 函数是否通过apply,call调用——this绑定的是指定对象
- 是否是隐式绑定,obj.foo(), this指向最后一级调用;
- 是否是默认绑定——window(非严格)/undefined(严格)
特例
有几个特例不太符合以上几种描述
1. 忽略this
function foo() {
console.log(this.a);
}
var a = 2;
foo.call(null); // 输出2,
null或undefined被传入call,apply或bind时会被忽略,变为默认引用。
用途:
bind柯里化foo.bind(null, 2),apply展开参数 foo.apply(null, [2,3])。
副作用:
如果foo真的使用了this,会产生错误,综合而言,容易产生Bug。
副作用解决——更安全的this:
借助Object.create(null)产生的空对象, Object.create(null)与{}的不同是不会创建Object.prototype的委托:
function foo(a, b) {
console.log("a: " + a + ",b: " + b);
}
var ø = Object.create(null);
foo.apply(ø, [2, 3]);
var bar = foo.bind(ø, 2);
bar(3); // a: 2, b: 3
2. this词法
ES6箭头函数无法使用以上规则,而是根据外层(函数或全局)作用域来决定this。
function foo() {
return (a) => {
// this继承自foo
console.log(this.a);
}
}
var obj1 = {
a: 1
}
var obj2 = {
a: 3
}
var bar = foo.call(obj1); // 返回箭头函数
bar.call(obj2); // 2 不是3
箭头函数的绑定无法被修改。
练习
这是阮大大在issue下回怼别人的一个例子,感觉很有趣,就放在这里:
function foo() {
return () => {
return () => {
return () => {
console.log("id:", this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()();
var t2 = f().call({id: 3})();
var t3 = f()().call({id: 4});
使用箭头函数的定义的函数this取决于f定义阶段,第一个箭头函数的this继承于foo,foo的this被硬绑定到了{id: 1}。并且箭头函数没有本身的this,所以内部的箭头函数的this逐级向上继承全部来自于foo。
答案:
id: 1
id: 1
id: 1
补充
为什么第一个例子this.count输出为NaN?
首先,这里是隐式绑定,this指向window,因此,RHS查询时,非严格模式下,产生了一个新的全局变量,值肯定是undefined,但是undefined + 1 = NaN,所以实际上foo中的this.count最终为NaN。
LHS与RHS为js引擎进行查询时的两种类型,L、R对应left,right,L为等号左侧,即被赋值对象查询,R为非等号左边,但不一定是赋值操作。函数传参具有隐式的LHS引用。
js中的深度拷贝
背景: javascript中,由于Object和Array这类引用类型的值存在,复制变量时若只是简单赋值,两个变量指向同一个堆变量,此时的赋值只是地址赋值。改变其中的一个对象,另外一个也会随之改变。因此,在开发时会有用到深拷贝的场景,例如react进行setState时。
浅拷贝
浅拷贝如前文所说,拷贝原对象的实例,但是对其内部的引用类型值,拷贝的是其引用。
var obj1 = {b:2};
var obj2 = obj1; // 此时只拷贝了对象的地址
console.log(o1===o2); // true
obj1.b = 3;
console.log(obj1); // 输出{b:3}
console.log(obj2); // 同样输出{b:3}
var arr1 = [1,2,3];
var arr2 = arr1;
arr1 === arr2; // true
arr2.push(4);
console.log(arr2); // [1,2,3,4]
console.log(arr1); // [1,2,3,4]
jquery中$.extend({}, obj),Array.prototype.slice()和Array.prototype.concat()返回对象或者数组的浅拷贝。
example:
var arr1 = ['iwtofly', {age: 22}];
var arr2 = arr1.slice();
console.log(o1 === o2); // false
console.log(o1[1] === o2[1]) // true
arr2[1].age = 23;
console.log(arr2[1].age); // 23
console.log(arr1[1].age); // 23
浅拷贝实现方法之一, 下段实现中的shallowClone:
module.exports = {
/**
* 获取类型
* @param {object/array/undefined/null/number/string/...} arg 待判断类型参数
* @return {String} 参数类型(首字母大写)
*/
getType: function(arg) {
let typeStr = Object.prototype.toString.call(arg);
return typeStr.replace(/\[object|\]|\s/g, '');
},
/**
* 浅拷贝--非递归
* @param {[type]} src 待拷贝的对象
* @return {[type]} 拷贝结果
*/
shallowClone: function(src) {
if (!src && typeof src !== 'object') {
console.log('it is not a object');
}
const type = this.getType(src);
let target = type === 'Array' ? [] : {};
for (let key in src) {
if (src.hasOwnProperty(key)) {
target[key] = src[key];
}
}
return target;
},
···
}
深拷贝
最简单的深拷贝是利用序列化和反序列化:
var target = JSON.parse(JSON.stringify(obj));
但是这种方法在序列化JavaScript对象时,所有函数和原型成员会被有意忽略,拷贝结果会丢失function、Date等类型。
除此之外,按照上面浅拷贝实现的思路只需要进行递归即可实现深拷贝如下:
module.exports = {
···
/**
* 深拷贝--递归
* @param {[type]} src 待拷贝的对象
* @return {[type]} 拷贝结果
*/
deepClone: function(src) {
if (!src && typeof src !== 'object') {
console.log('it is not a object');
}
const type = this.getType(src);
let target = type === 'Array' ? [] : {};
for (let key in src) {
if (src.hasOwnProperty(key)) {
const subType = this.getType(src[key]);
if (subType === 'Object' || subType === 'Array') {
// Object sub
target[key] = this.deepClone(src[key]);
} else {
target[key] = src[key];
}
}
}
return target;
}
}
结果测试
let Clone = require('../clone.js');
let o1 = ['iwtofly', {age: 23}, [1,2,3]];
let shallow = Clone.shallowClone(o1);
let deep = Clone.deepClone(o1);
console.log(shallow);
console.log(deep);
console.log('shallow is equal to o1? ',shallow === o1);
console.log('deep is equal to o1? ', deep === o1);
console.log('shallow[1] is equal to o1[1]? ',shallow[1] === o1[1]);
console.log('deep[1] is equal to o1[1]? ', deep[1] === o1[1]);
文件执行结果:
[ 'iwtofly', { age: 23 }, [ 1, 2, 3 ] ]
[ 'iwtofly', { age: 23 }, [ 1, 2, 3 ] ]
shallow is equal to o1? false
deep is equal to o1? false
shallow[1] is equal to o1[1]? true
deep[1] is equal to o1[1]? false
注意!!
- 有时突然想到深拷贝第一反应只会想到要区分Object类型,却忘记Array类型也要在初始化target时进行区分
- 区分数组和其他类型有多种方法,我常用的是下面的第一个方法
- Object的toString方法(Object.prototype.toString.call(obj)),再用正则去匹配,滤掉相同/公共的部分;
- object.constructor === Array, 数组的构造函数均为[Function: Array]
- for in会遍历出prototype中的属性
- 如果要求高的话,其实还需要单独判断Date、RegExp类型——new Date(src.getDate()),new RegExp(src.valueOf())。
- object.keys(),不会遍历原型链上的属性。
其他方法深拷贝详见:
https://github.com/iwtofly/FE-tools/blob/master/jsUtil.js
本方法实现:
https://github.com/iwtofly/FE-tools/blob/master/clone.js
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
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.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.