箭头函数
简单了解一下箭头函数
箭头函数的构成:
- ( ) : 参数
- => : 箭头
- { } : 函数的执行体
nums.forEach((item, index,arr) => {}) 一些常见的简写: 1. 如果参数只有一个,( ) 可以省略 num.forEach(item => { console.log(item) }) 2. 如果函数执行体只有一行代码,那么 { } 也可以省略 num.forEach(item => console.log(item)) 3. 如果只有一行代码且这行代码返回一个对象 var foo = () => ({ a: 1, b: 2})
注:箭头函数不会绑定this、arguments属性,不能与new一起使用,会报错
就是说上面四种this绑定方法对箭头函数都不适用,箭头函数的this取决于上层作用域
箭头函数中this的指向
var obj = { data: [], getData: function () { setTimeout(() => { var a = [1, 2, 3] this.data = a // 此处的this指向obj }, 2000) } } obj.getData()
扩:settimeout和forEach中的this指向
setTimeout(function () { console.log(this) }, 1000) // this 的指向是window var foo = [1, 2, 3] foo.forEach(function (item) { console.log(item, this); }, "a") // forEach 中this的指向是第二个参数
arguments
来自MDN对arguments的解释:
- arguments 是一个对应于传递给函数的参数的类数组对象。
- arguments对象是所有(非箭头)函数中都可用的局部变量。你可以使用arguments对象在函数中引用函数的参数
function foo(num1, num2, num3) { // 类数组对象中(长的像是一个数组, 本质上是一个对象): arguments console.log(arguments) // 1,2,3 } foo(1,2,3)
常见的对arguments的操作
- arguments.length : 得到本次函数调用时传入函数的实参数量。
arguments.length 表示的是实际上向函数传入了多少个参数,这个数字可以比形参数量大,也可以比形参数量小 (形参数量的值可以通过Function.length获取到).
function foo(num1, num2) { console.log(arguments.length) // 3 } foo(1,2,3)
- arguments[number] :根据索引值获取某一个参数
function foo(num1, num2) { console.log(arguments[2]) // 3 } foo(1,2,3)
- arguments.callee :获取当前arguments所在的函数
callee 是 arguments 对象的一个属性。它可以用于引用该函数的函数体内当前正在执行的函数。这在函数的名称是未知时很有用,例如在没有名称的函数表达式 (也称为“匿名函数”) 内。
function foo(num1, num2) { console.log(arguments.callee) } foo(1,2,3)
🚨警告: 在严格模式下,第 5 版 ECMAScript (ES5) 禁止使用 arguments.callee()。当一个函数必须调用自身的时候,避免使用 arguments.callee(),通过要么给函数表达式一个名字,要么使用一个函数声明。
因为arguments是类数组,它没有 Array的 内置方法, 例如forEach()和 map()都是没有的。
所以要将arguments转成array来使用
将arguments转成array的方法
- 自己遍历元素然后加进新的数组里面
function foo(num1, num2) { var newArr = [] for (var i = 0; i < arguments.length; i++) { newArr.push(arguments[i] * 10) } console.log(newArr) } foo(1,2,3) 2. Array.prototype.slice 或是[ ]将arguments转成array function foo(num1, num2) { var newArr2 = Array.prototype.slice.call(arguments) console.log(newArr2) var newArr3 = [].slice.call(arguments) console.log(newArr3) } foo(1,2,3)
🚨警告: 对参数使用 slice 会阻止某些 JavaScript 引擎中的优化,所以为了提高性能,不推荐使用 slice 来进行数组转换
- ES6 的语法
function foo(num1, num2) { var newArr4 = Array.from(arguments) console.log(newArr4) var newArr5 = [...arguments] console.log(newArr5) } foo(1,2,3)
箭头函数没有arguments
因为箭头函数中没有arguments,如果在箭头函数中输入console.log(arguments)
,它会去上层函数去找,如果有则输出,没有则报错
🤔如果我们传的实参比形参多,并想要拿到这些额外的参数,该怎么办?
可以使用剩余参数
var foo = (num1, num2, ...args) => { console.log(args) } foo(1,2,3,4,5)
备注: 如果你编写兼容 ES6 的代码,那么优先推荐使用 剩余参数
剩余参数和arguments对象的区别
- 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参。
// 剩余函数 function foo(num1, num2, ...args) { console.log(args) } foo(1,2,3,4,5) // arguments function foo(num) { console.log(arguments) }
foo(1,2,3,4,5)
- arguments对象不是一个真正的数组,而剩余参数是真正的 Array实例,也就是说你能够在它上面直接使用所有的数组方法,比如forEach 等
- arguments对象还有一些附加的属性 (如callee属性)。
柯里化
什么是柯里化
// 我们常见的函数 function add(x, y, z) { return x + y + z } add(1,2,3)
柯里化就是:指的是将一个接受多个参数的函数 变为 接受一个或部分参数返回一个函数的固定形式,这样便于再次调用
上代码:
// 柯里化的函数 function sum(x) { return function(y) { return function(z) { return x + y + z } } } add(1)(2)(3)
这样从原来的接收多个参数的函数转 变成 接收一个单一(部分)参数的函数 并 返回接收剩余参数进而返回结果的新函数
感觉好像把原本简单的函数复杂化了,我们加点箭头函数:
// 简化柯里化的代码 var sum = x => y => z => { return x + y + z } sum(1)(2)(3)
🤔 看起来是变简单了,但将函数柯里化有什么好处呢?我用原来的不香吗?
下面就来讲讲柯里化的好处
为什么要柯里化
好处一:单一职责
在设计模式中就有提到单一职责原则,通俗的说,即一个类只负责一项职责。而我们的柯里化的好处就有一项是单一职责:一个函数处理的问题尽可能的单一。我们可以看到上面柯里化后有很多个函数,就是用这些函数来一个一个地去处理问题,处理完后再在下一个函数中使用处理后的结果
function sum(x){ x = x + 1 return function (y){ y = y * 2 return x + y } } console.log(sum(1)(2))
当某一层的函数输出结果出现问题的时候就可以到对应的地方去修改就可以了
好处二:逻辑的复用
定制函数,将重复使用的参数定制成一个函数使用,减少代码量和工作量
// 场景:我们经常需要将5与其他数字相加 // 未复用前 function sum(m,n){ m = m * m return m + n } console.log(sum(5,10)) console.log(sum(5,100)) console.log(sum(5,1000)) console.log(sum(5,10000)) // 柯里化后 function sum(m){ m = m * m return function(n){ return m + n } } var add5 = sum(5) add5(10) add5(100) add5(1000) add5(10000)
自动柯里化函数的实现
目的:通过封装柯里化函数来实现传入一个普通函数自动柯里化的功能
柯里化函数的实现
function myCurrying(fn) { // 不确定接收几个参数就用...args来接收 function curried(...args){ // 判断当前已经接受的参数的个数,和参数本身需要接受的参数是否已经一致了 // 一旦接收完后就不再接收了 if(args.length >= fn.length){ // 如果调用的时候有绑定this,就直接绑定到fn上 return fn.apply(this,args) // return fn.call(this,args) 也可以 }else{ // 新函数接收后面的参数 function curried2(...args2) { // 后来的参数要与之前的函数进行拼接,然后再递归调用 return curried.apply(this,args.concat(args2)) } return curried2 } } return curried }
自动柯里化函数的使用
// 要进行柯里化的普通函数 function sum(m,n){ m = m * m return m + n } // 自动柯里化函数的封装 function myCurrying(fn) { ... } var currySum = myCurrying(sum) console.log(currySum(10)(10)) //110 console.log(currySum(10,10)) //110
普通函数使用柯里化的注意事项
如果是一个一个参数传的话就不能传入多于原本普通函数所需的参数,否则会将前面的看做是一个函数从而报错
// 要进行柯里化的普通函数 function sum(m,n){ m = m * m return m + n } // 自动柯里化函数的封装 function myCurrying(fn) { ... } var currySum = myCurrying(sum) console.log(currySum(10)(10)(1))
而如果输入的参数少于原本普通函数所需的参数,则会返回一个接受剩余参数的函数
var currySum = myCurrying(sum) console.log(currySum(10)) // [Function: curried2]
利用这一特点补充一下柯里化的第三个好处
好处三:延迟运行/计算
要求:柯里化了后的函数,它返回一个新的函数,新的函数接收可分批次接受新的参数,延迟到最后一次计算,我们可以任意传入参数,当不传参数的时候,输出计算结果
function currying(fn) { var allArgs = []; // 用来接收参数 return function next(...args) { var args1 = [].slice.call(args); // 判断是否执行计算 if (args1.length > 0) { allArgs = allArgs.concat(args1); // 收集传入的参数,进行缓存 return next; } else { return fn.apply(null, allArgs); // 符合执行条件,执行计算 } } }
补充slice( )
的说明 : slice() 方法提取某个字符串的一部分,并返回一个新的字符串,且不会改动原字符串。
使用1:
function sum(m, n) { m = m * m return m + n } function currying(fn) { ... } var currySum = currying(sum) console.log(currySum(10)(10)(1)(10)()) // 输出结果:110 // 传入多少个都无所谓,到时候只要需要的参数就行,也不会报错 // 后面一定要加上括号,因为传入空参数是执行的条件,不加则会返回一个接受剩余参数的函数
使用2:
function sum(...args) { var m = 0 for (var i = 0; i < args.length; i++) { m += args[i]; } return m; }; function currying(fn) { ... } var currySum = currying(sum) console.log(currySum(10)(10)(1)(10)()) // 输出结果:31
参考文章:
https://www.imooc.com/article/46624
with语句、eval函数
with语句
with 语句: 可以形成自己的作用域
原先:
var obj = {name:'巴咔巴咔',age:1,message:'hello world'} var message = 'bye world' function foo(){ function bar(){ console.log(message) } bar() } foo() // 输出: bye world // 沿着作用域链一层一层往上查找,直到找到 with语句后: var obj = {name:'巴咔巴咔',age:1,message:'hello world'} var message = 'bye world' function foo(){ function bar(){ with(obj){ console.log(message) } } bar() } foo() // 输出: hello world // 先在with括号内指定的对象进行查找,找不到就再往上层作用域找 注意1: 单独一个对象不会形成一个作用域 注意2:在严格模式下with语句
所以with语句在严格模式下是行不通的。
在MDN的开头就写了:不建议使用with语句,因为它可能是混淆错误和兼容性问题的根源。
所以主要是了解一下有with语句这么一个东西就行,在现实开发中不推荐使用
eval函数
eval()
函数会将传入的字符串当做 JavaScript 代码进行执行。
var jsString = 'var message = "Hello World"; console.log(message);'
eval(jsString) // Hello World
有时候在webpack生成的临时代码会看到,但在打包生成的文件中是不会有eval函数代码的
在MDN上也有写到:永远不要使用eval!
原因有这么几个:
- eval代码的可读性非常的差(代码的可读性是高质量代码的重要原则); (可读性差)
- eval是一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险; (安全性低)
- eval的执行必须经过JS解释器,不能被JS引擎优化 (效率低)
面向对象中与对象相关的知识
我们经常调侃:”没有对象,自己new一个“ 。 而在JavaScript中对象是什么呢?
面向对象
面向对象是相对于面向过程来讲的,面向对象方法,把相关的数据和方法组织为一个整体来看待,从更高的层次来进行系统建模,更贴近事物的自然运行模式(面向对象就是现实的抽象方式)
一句话来说:
面向对象是以对象功能来划分问题,而不是步骤。
面向对象对于我们编程是很重要的一个东西:因为我们在大部分情况下描述一个事物都是从整体来描述的
对象
对象可以将多个相关联的数据封装到一起,更好的描述一个事物:
var girlFriend ={ // key: value name: '巴咔巴咔', height: 170, age: 18 ... }
JavaScript中的对象被设计成一组属性的无序集合,由key和value组成;
- key是一个标识符名称,
- value可以是任意类型,也可以是其他对象或者函数类型,如果值是一个函数,那么我们可以称之为是对象的方法;
创建对象的方法
new Object
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例
// new一个对象本质是创建一个空对象,然后往里面添加属性和方法 var girlFriend = new Object() girlFriend。name: '巴咔巴咔' girlFriend.height = 170 girlFriend.age = 18
字面量创建
var girlFriend ={ name: '巴咔巴咔', height: 170, age: 18 ... }
创建单个对象的方法弊端:需要编写的代码重复的地方很多,代码冗杂
创建多个对象的方法
工厂模式 —— 一种常见的设计模式
工厂中的代码写一次便可以复用无数次,能够减少代码量
先写一个工厂函数接收方法,返回对象,接下来就只用传入不同的参数就好了
function createObj(name, age) { var obj = {} obj.name = name obj.age = age obj.eating = function() { console.log(this.name + "在吃东西~") } return obj } var p1 = createObj("张三", 18) var p2 = createObj("李四", 20)
缺点:工厂模式创建的对象属于Object,无法区分对象类型,这也是工厂模式没有广泛使用的原因
构造函数(constructor)
普通的函数被使用new操作符来调用了,那么这个函数就可以被称之为构造函数
规范:构造函数定义时首字母大写 (因为是通过调用的方式来确定是否为构造函数,单纯看函数区分不了,所以要通过首字母大写来区分这个函数为构造函数)
function foo() { ... } // foo是普通函数 foo() function Foo() { ... } // Foo是构造函数 new Foo()
foo被new调用之后会执行:
- 申请内存,创建对象
- 这个对象内部的
[[prototype]]
(浏览器中提供的__proto__
)会被赋值为该构造函数的prototype属性; - 构造函数内部的this,会指向创建出来的新对象;(new绑定)
- 执行函数的内部代码(函数体代码);
- 如果构造函数没有返回非空对象,则返回创建出来的新对象;(无论如何new 构造函数之后都会返回一个对象值,而普通函数没有返回值则返回undefined)
//手写构造函数 function Test (constructFunction){ let obj = {}; obj.__proto__ = constructFunction.prototype; return function(){ constructFunction.apply(obj,arguments); return obj; } } 使用: function Person(name, age) { this.name = name this.age = age this.eating = function() { console.log(this.name + "在吃东西~") } // 不用返回对象,构造函数自动返回 } var p1 = new Person("张三", 18) var p2 = new Person("李四", 20)
缺点: 两次构造的对象是不一样的(就算你的操作是一样的,他们构建的对象也是不一样的),因为这个特性每次构造都是一个新的函数对象,就会造成不必要的空间浪费
如何优化上面的构造函数——就要用到“原型”
对象的原型/隐式原型(不会直接使用)
我们每个对象中都有一个 [[prototype]], 这个属性可以称之为对象的原型(隐式原型)
怎么查看:
__proto__
(浏览器提供):console.log(obj.__proto__
)- Object.getPrototypeOf (ES5之后提供) :console.log(Object.getPrototypeOf (obj))
用途:我们找一个对象里面获取一个属性,在当前对象中找不到就会去原型里面查找。(方便实现继承,在多个对象里面要使用同一个属性或方法时就可以将其放到原型中,而不是放到构造函数中去浪费空间)
函数的原型/显示原型
函数作为对象来说,他也有隐式原型[[prototype]],但函数因为是一个函数,所以他还有一个显示原型prototype
function Obj() { } var obj1 = new Obj() var obj2 = new Obj()
构造函数创建对象的时候会把显示原型赋值给隐式原型:
就是Obj中有prototype且prototype存着原型对象的地址,指向原型对象的地址;构建对象的时候将prototype赋值给obj1和obj2的__proto__
,这样obj1和obj2的__proto__
也指向原型对象(obj2.__proto__
=== Obj.prototype)
🚨注意:Obj.prototype !== Obj.prototype
(一个构造函数的显式原型和自己的隐式原型是不相等的,注意与上面区分)
因为 Obj.prototype = { constructor: Obj }
而 Obj.__proto__ = Function.prototype
Function.prototype = { constructor: Function }
原型关系:
原型对象中有一个constructor属性,而这个constructor属性指向当前的函数对象(Obj)
现在我们了解了原型的概念,知道了构造函数的弊端是会创建重复的函数,那么就来尝试优化一下构造函数吧: