场景
将重复的函数方法/属性放进原型中再进行调用(注意:得是重复的才能放进原型,不重复的放入会被后来的覆盖,所以一般来说是函数方法放入原型,属性很少)
function Person(name, age) { this.name = name this.age = age } Person.prototype.eating = function() { console.log(this.name + "在吃东西~") } Person.prototype.running = function() { console.log(this.name + "在跑步~") } var p1 = new Person("张三", 18) var p2 = new Person("李四", 20) p1.eating() p2.eating()
操作对象的方法
var obj ={ name: '巴咔巴咔', height: 170, age: 18 ... } // 获取属性 console.log(obj.name) // 给属性赋值 obj.height = 172 // 删除属性 delete obj.name // 遍历属性 for (var key in obj) { console.log(key) }
但有时候我们不想让别人这么轻易地对 ‘对象中的属性’ 进行操作,对别人的操作进行限制,就要用到Object.defineProperty
Object.defineProperty()
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
Object.defineProperty(obj,prop,descriptor) 中的参数分别对应 '要定义属性的对象'、'要定义或修改的属性/Symbol'、'属性描述符'
,返回值:被传递给的函数的对象
而其中的属性描述符又分为数据描述符和存取描述符
configurable |
enumerable |
value |
writable |
get |
set |
|
数据描述符 |
T |
T |
T |
T |
F |
F |
存取描述符 |
T |
T |
F |
F |
T |
T |
- [configurable]:表示属性是否可配置(删除、修改特性、改为存取属性描述符)
- [enumerable]:有可枚举的意思(能否通过for-in或Object.keys( )来返回属性)
- [value]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改; 默认情况下这个值是undefined;
- [writable]:表示能否修改属性的值
- [get]:获取属性时会执行的函数。默认为undefined
- [set]:设置属性时会执行的函数。默认为undefined
数据属性描述符:
var obj ={ name: '巴咔巴咔', height: 170, age: 18 } // 数据属性描述符 Object.defineProperty(obj, "address", { value: "北京市", // 默认值undefined // 该特性不可删除/也不可以重新定义属性描述符 configurable: false, // 默认值false // 该特性是配置对应的属性(address)是否是可以枚举 enumerable: true, // 默认值false // 该特性是属性是否是可以赋值(写入值) writable: false // 默认值false }) // 测试configurable的作用 delete obj.address // 报错 // 测试enumerable的作用 for (var key in obj) { console.log(key) // name height age address } // 测试writable的作用 obj.address = "上海市" console.log(obj.address) // 北京市 (这是一个静默错误)
存取属性符使用场景:
1.隐藏某一个私有属性不希望直接被外界使用和赋值(如下面_address
就被隐藏起来了,别人使用的是address
,虽然说_address
是私有属性,但JS里没有严格意义的私有属性,就是说console.log
还是可以看到,但程序员看到了以下划线开头的就知道是私有属性/方法而不会在外面直接使用它,只能说是默认的规范)
var obj ={ name: '巴咔巴咔', height: 170, age: 18, _address: "北京市" } // 存取属性描述符 Object.defineProperty(obj, "address", { enumerable: true, configurable: true, get: function() { return this._address }, set: function(value) { this._address = value } }) console.log(obj.address) obj.address = "上海市" console.log(obj.address)
2.如果我们希望截获某一个属性它访问和设置值的过程时, 也会使用存储属性描述符(vue中响应式的原理)
var obj ={ name: '巴咔巴咔', height: '170', age: '18', _address: "北京市" } Object.defineProperty(obj, "address", { enumerable: true, configurable: true, get: function () { foo() return this._address }, set: function (value) { bar() this._address = value } }) console.log(obj.address) obj.address = "上海市" console.log(obj.address) function foo() { console.log("获取了一次address的值") } function bar() { console.log("设置了addres的值") }
注意:有writable和value就不能有get和set
Object.defineProperties()
上面的Object.defineProperty()
只能定义一个属性,而Object.defineProperties()
可以定义多个属性
Object.defineProperties(obj, { name: { configurable: true, enumerable: true, writable: true, value: "玛卡巴卡" }, age: { configurable: true, enumerable: true, get: function() { return this._age }, set: function(value) { this._age = value } } })
补充
- 获取某一个特性属性的属性描述符 :
console.log(Object.getOwnPropertyDescriptor(obj,"name"))
- 获取对象的所有属性描述符:
console.log(Object.getOwnPropertyDescriptors(obj))
- 禁止对象继续添加新的属性:
Object.preventExtensions(obj)
- 让属性不可以修改(writable: false)
Object.freeze(obj)
......
- hasOwnProperty方法判断对象中是否含有某个属性(不是原型上的属性)
console.log(info.hasOwnProperty("address"))
console.log(info.hasOwnProperty("name"))
6. in 操作符: 判断对象中是否含有某个属性(不管在当前对象还是原型中返回的都是true)
console.log("address" in info)
console.log("name" in info)
- instanceof 用于判断构造函数的pototype,是否出现在某个实例对象的原型链上
console.log(stu instanceof Student)
// 判断Student的原型是否出现在stu的原型链上
JavaScript的原型链
JavaScript原型链
从一个对象上获取属性,如果在当前对象中没有获取到就会去它的原型(__proto__)上面获取([get]操作)
var obj = { a: 1 } // 原型链 obj.__proto__ = { } obj.__proto__.__proto__ = { b:2 }
console.log(obj.b) // 会沿着原型链依次往下找
那如果里面没有这个属性呢,这样不就会一直找下去了吗?
不会的,Object的原型链也是有尽头的
原型链的尽头——[Object: null prototype] {}
但我们打印Object.__proto__
的日志的时候发现控制台打印出[Object: null prototype] {} ,就可以知道已经找到原型链的尽头了,再console.log(Object.__proto__.__proto__)
控制台就会打印null
例1:有我们想要找的属性
var obj = { a: 1 } // 原型链 obj.__proto__ = { } obj.__proto__.__proto__ = { b:2 } console.log(obj.b) // 会沿着原型链依次往下找 // 这个例子原型链的尽头 console.log(obj.__proto__.__proto__.__proto__) //[Object: null prototype] {} console.log(obj.__proto__.__proto__.__proto__.__proto__)//null
例2:没有我们想要找的属性
var obj = { a: 1 } obj.__proto__ = { } console.log(obj.b) //undefined console.log(obj.__proto__) //{} console.log(obj.__proto__.__proto__) //[Object: null prototype] {}
总结:我们没有往obj
的原型[__proto__]上额外去添加属性,那么它下一个原型[__proto__]就是原型链的尽头——[Object: null prototype] {}
例3:对于构造函数的原型链
function Person() { } console.log(Person.prototype.__proto__) //[Object: null prototype] {} console.log(Person.prototype.__proto__.__proto__) //null
[Object: null prototype] {} 原型有什么特殊吗?
- 特殊一:该对象有原型属性,但是它的原型属性已经指向的是null,也就是已经是顶层原型了;
- 特殊二:该对象上有很多默认的属性和方法;
扩:__proto__和prototype的区别(更加详细的可去看上篇文章的内容)
本来是没有__proto__这个东西的,是浏览器加的,就是为了让你能找到一个实例的构造函数的原型
__proto__代表指针和prototype代表主体,prototype.__proto__指向下一个prototype
(例:__proto__指向父母,prototype代表父母自己)
JavaScript的继承实现
面向对象的特性——继承
面向对象有三大特性:封装、继承、多态
- 封装:我们前面将属性和方法封装到一个类中,可以称之为封装的过程;
eg:编写构造函数的过程可以称之为是一个封装的过程
function Person(name, age) { this.name = name this.age = age this.eating = function() { console.log(this.name + "在吃东西~") } }
- 继承:不仅可以重复利用一些代码(对代码的复用),也是多态前提(纯面向对象中);
- 多态:不同的对象在执行时表现出不同的形态;
继承
继承可以帮助我们将重复的代码和逻辑抽取到父类中,子类只需要直接继承过来使用即可。
原型链继承(在实践中不会使用,但后续优化的方法是基于它实现的)
// 父类: 公共属性和方法 function Person() { this.name = "a" } Person.prototype.eating = function() { console.log(this.name + " eating~") } // 子类: 特有属性和方法 function Student() { this.sno = 111 } Student.prototype = new Person() Student.prototype.studying = function() { console.log(this.name + " studying~") } var stu = new Student() console.log(stu.name) stu.eating()
当还没有继承前(上面代码去除Student.prototype = new Person()),父类和子类还是两个独立的函数:
当加上Student.prototype = new Person()后,两个函数在内存中发生的变化:
- 重写了Student的原型
- 让子类原型不再指向子类构造函数
- 子类原型上的属性被继承的属性所覆盖
(也是因为是重写了Student的原型,所以Student在原型上的方法不能写在Student.prototype = new Person()
之后,不然它会被继承的属性所覆盖)
- 其原型指针__proto__指向了父类的原型对象,这样子类就可以沿着原型链访问到父类的方法eating。
- 子类原型是父类实例,通过父类构造函数,子类原型继承了父类的属性,最终,子类继承了父类的方法和属性,所以子类原型对象中有属性name = ‘a’
原型链继承的弊端:
- 打印stu对象时,继承的属性看不到(因为继承的属性在原型上,而原型上的属性不可枚举)
- 父类的实例属性会被子类所有实例共享
- 如果该属性是基本类型值时则没有问题
- 如果这个对象是一个引用类型(比如数组),那么就会造成问题[ 修改实例1的该属性(比如向数组push一个新值),实例2也会跟着改变。]
- 不好将参数传给父类
借用构造函数继承
做法:在子类型构造函数的内部调用父类型构造函数,创建子类实例会执行子类的构造函数(含父类的构造函数),也就完成了继承
// 父类: 公共属性和方法 function Person(name, age, friends) { // this指代的是stu this.name = name this.age = age this.friends = friends } // 子类: 特有属性和方法 function Student(name, age, friends, sno) { Person.call(this, name, age, friends) // 将上面的函数当成普通函数进行调用 this.sno = 111 } var stu = new Student("a", 18, ["kobe"], 111) console.log(stu)
解决原型链继承的弊端:
- 解决继承的属性打印不出来问题:因为借用构造函数进行继承在子类构造函数中调用父类的构造函数完成继承,与原型对象无关,所以打印出来的属性也是可枚举的
- 解决传参的问题 :通过apply()和call()方法给父类传参
- 解决共享的问题(实例与实例之间不会相互影响):把
this
指向改成了指向新的实例,所以就会把 Person里面的this
相关属性和方法赋值到新的实例上,而不是赋值到 Student 原型上面
虽然借用函数继承能解决原型链继承的问题,但它也带来了新的问题:
- 子类实例不能访问父类原型对象中的属性和方法,因为借用构造函数进行继承在子类构造函数中调用父类的构造函数完成继承,与原型对象无关,所以它是继承父类构造函数中的属性,而没有继承父类原型上的属性 (有人提出可以通过用
Student.prototype = Person.prototype
来解决,但是这样在子类原型上添加的方法也会加到父类的原型里面去,违背了面向对象的初衷) - 无法实现函数复用,由于 call 有多个父类实例的副本,性能损耗。
原型式继承
原型式继承是针对对象的一种继承
简单实现一下原型式继承(实现的是对象的继承)
// 目的:让info的原型指向obj对象 var obj = { name: "a", age: 18 } var info = Object.create(obj) console.log(info.__proto__) // {name: "a", age: 18} // 上面代码的本质如下: var obj = { name: "a", age: 18 } // 原型式继承函数 function createObject(o) { var newObj = {} Object.setPrototypeOf(newObj, o) // 将传入的o作为newObj的原型 return newObj } var info = createObject(obj) console.log(info.__proto__) // {name: "a", age: 18}
寄生式继承(存在工厂函数一样的弊端,了解即可)
原型式继承 + 工厂函数 var personObj = { running: function() { console.log("running") } } // 工厂函数 function createStudent(name) { var stu = Object.create(personObj) // 原型式继承 stu.name = name stu.studying = function() { console.log("studying~") } return stu } var stuObj = createStudent("why") var stuObj1 = createStudent("kobe") var stuObj2 = createStudent("james")
寄生组合式继承(最终方案)
我们再来回顾一下我们的目的:子类要继承父类的原型且往子类添加属性或方法不影响到父类
// 工具函数: 实现封装(原型式继承 + 指定子类的constructor为自身) function inheritPrototype(SubType, SuperType) { SubType.prototype = Object.create(SuperType.prototype) Object.defineProperty(SubType.prototype, "constructor", { enumerable: false, configurable: true, writable: true, value: SubType }) } // 要继承的父类 function Person(name, age, friends) { this.name = name this.age = age this.friends = friends } Person.prototype.running = function() { console.log("running~") } Person.prototype.eating = function() { console.log("eating~") } // 子类 function Student(name, age, friends, sno, score) { Person.call(this, name, age, friends) this.sno = sno this.score = score } inheritPrototype(Student, Person) Student.prototype.studying = function() { console.log("studying~") }
参考:
https://juejin.cn/post/6934498361475072014
https://www.jianshu.com/p/0045cd01e0be
面向对象的多态
面向对象的三大特性:封装、继承、多态。
前面两个我们都已经详细解析过了,接下来我们讨论一下JavaScript的多态。
多态
多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口,或使用一
个单一的符号来表示多个不同的类型 【来自:维基百科】
实际上是不同对象作用于同一操作产生不同的效果。
传统的面向对象多态是有三个前提:
- 必须有继承(是多态的前提)
- 必须有重写(子类重写父类的方法,输出的时候才会有不同的表现)
- 必须有父类引用指向子类对象
ts中多态的体现:(ts的多态偏向于传统的面向对象多态)
// Shape形状 class Shape { getArea() {} } class Rectangle extends Shape { // 继承 getArea() { return 100 // 重写了父类的方法 } } class Circle extends Shape { getArea() { return 200 // 重写了父类的方法 } } var r = new Rectangle() var c = new Circle() // 多态: 当对不同的数据类型执行同一个操作时, 如果表现出来的行为(形态)不一样, 那么就是多态的体现. function calcArea(shape: Shape) { console.log(shape.getArea()) } calcArea(r) // 父类引用指向子类对象 calcArea(c) 但因为JavaScript是动态的语言,所以JavaScript中对多态的要求会比较宽松 // 也是多态的体现 function sum(m, n) { return m + n } sum(20, 30) sum("abc", "cba")
ECMAScript相关
大小写
ECMAScript中的一切都是区分大小写的,无论是变量操作符还是函数名,如:Typeof 和 typeof 是不一样的
严格模式
ES5中增加,是一种不同的JS解析和执行模型,通过在脚本开头或者函数体内部增加use strict
字符这个预处理指令,开启严格模式,一些不规则的(ECMA3)写法在这里会抛出异常。
设立"严格模式"的目的,主要有以下几个:
- 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
- 消除代码运行的一些不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的Javascript做好铺垫。
开启严格模式:
- 严格模式通过在脚本或函数的头部添加
use strict
; 表达式来声明。 use strict
并不严格限制一定要写在第一行。前边无产生实际运行结果的语句,use strict
可以不在第一行
// 整个脚本都开启严格模式的语法 "use strict"; var v = "Hi! I'm a strict mode script!"; //单独在某个函数内开启严格模式 function a (){ "use strict"; //函数体 } //use strict可以不在第一行 function(){ "asd"; "use strict"; a = 1; }
严格模式 vs 正常模式
正常模式 |
严格模式 |
|
一个变量没有声明就赋值 |
默认是全局变量 |
全局变量必须显式声明(先用var命令声明,然后再使用) |
对于静默失败(不报错也没有任何效果)的赋值操作 如:给NaN赋值 |
不给效果也不报错 |
报错 |
试图删除(不可删除的属性/变量/对象/函数)
|
不给效果也不报错 |
报错 |
对象有多个重名属性 |
允许重名属性,只有最后一个属性起作用 |
报语法错误 |
函数有多个重名的参数 function f(a, a, b) { return ; } |
可以用arguments[i]读取 |
报语法错误 |
八进制数字语法 |
整数的第一位如果是0,表示这是八进制数,比如0100等于十进制的64 |
禁止八进制数字语法,使用会报语法错误 |
严格模式下的eval和arguments
eval
- 变量名不能使用 "eval" 字符串(不能通过程序语法被绑定 (be bound) 或赋值)
"use strict"; var eval = 1; // 报错 2. eval作用域: 在eval里面生成的变量只能用于eval内部 "use strict"; var a = 1; console.info(eval("var a = 2; a")) // 2 console.info(x) //1 eval ("var a = 2"); alert (a); // 报错
arguments
arguments是函数的参数对象
- arguments 不能通过程序语法被绑定 (be bound) 或赋值
"use strict"; arguments++; // 语法错误 var obj = { set p(arguments) { } }; // 语法错误 try { } catch (arguments) { } // 语法错误 function arguments() { } // 语法错误 var f = new Function("arguments", "'use strict'; return 17;"); // 语法错误
- 参数的值不会随 arguments 对象的值的改变而变化
function f(a) { a = 2; return [a, arguments[0]]; } f(1); // 正常模式为[2,2] function f(a) { "use strict"; a = 2; return [a, arguments[0]]; } f(1); // 严格模式为[2,1]
- 不再支持 arguments.callee
"use strict"; var f = function() { return arguments.callee; }; f(); // 报错
🚨 ES6 的模块自动采用严格模式,不管你有没有在模块头部加上`"use strict";`。
参考资料:
菜鸟教程:https://www.runoob.com/js/js-strict.html
《JavaScript高级程序设计》
MDN: https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Strict_mode
阮一峰的网络日志:https://www.ruanyifeng.com/blog/2013/01/javascript_strict_mode.html
ES6的学习分享
对于ES6的学习我是很推荐去看看阮一峰的ES6 入门教程 :https://es6.ruanyifeng.com/
在学习ES6之前先明确一些概念和常识有助于更好的学习ES6
为什么学习ES6
- ES6相当于是JS的语法,没有本质改变。
- 现在开发普遍使用ES6,需要看得懂
- es6的新语法,让我们在开发中,相比之前传统的js简便不少,新的规则也让编码变得越来越规范
一些常识
- ES6, 全称 ECMAScript 6.0 ,是 JavaScript 语言的下一代标准
- ES和JS的区别:ES是标准,JS是实现。
- ES5,ES6和ES2015的区别:ES5泛指上一代JS语言标准,ES6泛指下一代JS语言标准,ES2015是ES5和ES6的分界线。现阶段在绝大部分场景下,ES2015默认等同于ES6。
class
class定义类
实际上,类是“特殊的函数”,就像你能够定义的函数表达式和函数声明一样,类语法有两个组成部分:类表达式和类声明。
本质依旧是构造函数、原型链的语法糖
// 类的声明 class Person { } // 类似于: function Person { } // 还有一个:类的表达式(但不常用) var a = class {}
注意:函数声明和类声明之间的一个重要区别在于,函数声明会提升,类声明不会。你首先需要声明你的类,然后再访问它(类表达式也是先声明后访问)
类的特性(和构造函数是一样的)
- 有显示原型prototype,也有隐式原型__proto__,还有constructor
- 通过typeof操作 类 打印出来的是
function
而不是class
class的构造方法
类的作用在于构建对象,而constructor是一种用于创建和初始化class创建的对象的特殊方法。
一个类只能有一个构造函数 (多个会报错)
class Person { constructor(name, age) { this.name = name this.age = age } } var p = new Person("a", 18)
如果不指定一个构造函数 (constructor) 方法,则使用一个默认的构造函数 (constructor)。
类的普通实例方法
一般通过创建出来的对象进行访问
类的实例方法定义:在class里定义方法相当于在原型上定义
class Person { constructor(name, age) { this.name = name this.age = age } eating() { console.log(this.name + " eating~") } } var p = new Person("a", 18) p.eating()
类的访问器方法定义:在class中定义 getter 和 setter 函数
class Person { constructor() { this._address = "广州市" } // 类的访问器方法 get address() { console.log("拦截访问操作") return this._address } set address(newAddress) { console.log("拦截设置操作") this._address = newAddress } }
类的静态方法
static 关键字用来定义一个类的一个静态方法。
通过类名进行访问,不用创建实例
通常用于为一个应用程序创建工具函数。
class Foo { static displayName = "Point"; static classWrite() { console.log("hello world!"); } classWrite() { console.log("你好,世界!"); } } const p1 = new Foo(); console.log(p1.displayName) // undefined Foo.classWrite();// hello world!
如果在实例上调用静态方法,就会抛出错误,表示该方法不存在。
因为静态方法只能通过类来调用,实例无法继承它的方法。
所以说如果静态方法中包含 this 关键字,通过类调用则这个 this 指向的是类,而不是实例。
在前面的文章可以看出我们实现继承是很麻烦的,但用class来实现继承很简单
用class实现继承
使用 extends关键字在 类声明 或 类表达式 中创建一个类作为另一个类的一个子类。
以前传参数给父类我们使用的是call 方法,现在则要使用super关键字(语法要求,不使用会报错)
super的使用:调用 父类 的构造函数并传参、调用 父类 上的方法
class Person { constructor(name) { this.name = name; } speak() { console.log(`${this.name} makes a noise.`); } } // Student称之为子类(派生类) class Student extends Person { // Student 继承 Person constructor(name) { super(name); // 调用父类构造函数并传入 name 参数 } speak() { console.log(`${this.name} barks.`); } } var p = new Student('a'); p.speak();// 'a barks.'
子类可以继承父类的方法,但如果子类重写了父类的方法,则打印的时候会输出子类重写后的方法
🚨注意:在子(派生)类的构造函数中使用this或者返回默认对象之前,必须先通过super调用父类的构造函数。
ES6转ES5的代码(了解)
因为有一些较低版本的浏览器不能识别比较新的语法,所以我们要将它转成ES5从而让浏览器识别(使用babel工具可以将新的JS/TS代码转成ES5的代码)
babel在线网站:https://babeljs.io/
继承内置类
继承系统的类并对其进行扩展
以array为例:
class MyArray extends Array { eating(){ console.log("eatting~") } } const a = new MyArray(1, 2, 3); const b = a.map(x => x); // MyArray 继承了 Array 的方法,可以使用map方法 b instanceof MyArray; // true console.log(b); // MyArray(3) [ 1, 2, 3 ] console.log(a.eating); // [Function: eating]
类的混入mixin
因为JavaScript的类只支持单继承,即只能继承一个父类
但我们想要继承多个父类,则该功能必须由父类提供
一个以超类作为输入的函数和一个继承该超类的子类作为输出可以用于在 ECMAScript 中实现混合:
function mixinRunner(BaseClass) { class NewClass extends BaseClass { // 可以指定类的名称 running() { console.log("running~") } } return NewClass } function mixinEater(BaseClass) { return class extends BaseClass { // 也可以不指定类的名称 eating() { console.log("eatting~") } } } class Person { } class Student extends mixinEater(mixinRunner(Person)) {} var p = new Student() p.running() // running~ p.eating() // eatting~
在上述代码中,虽然没有mixin关键字,但我们利用JS原有的特性来实现混入的效果。
ES6知识点讲解
ES6对象字面量增强
- 属性的简写
var name = "a" var obj = { name // 当 name: name 时 ,可以直接写成 name }
- 方法的简写
var obj = { foo: function() { console.log(this) }, // 下面是上面代码的简写 bar() { console.log(this) }, // 注意与箭头函数区分,箭头函数的this和上面的this是不一样的 baz: () => { console.log(this) } }
- 添加计算属性名
var name = "a" var obj = { // ES6之后的做法 [name + 123]: 'hehehehe' // { a123: 'hehehehe' } } //ES6之前的做法: obj[name + 123] = "hahaha" // { a123: 'hahaha' }
解构赋值(常见的两个)
数组解构赋值
对数组的解构: [ ] 数组的元素是按次序排列的,变量的取值是由它的位置决定的 var names = ["abc", "cba", "nba"] // 不使用解构 var item1 = names[0] var item2 = names[1] var item3 = names[2] // 使用解构 var [item1, item2, item3] = names // 解构后面的元素 var [, , item4] = names console.log(itemz) // 解构出一个元素,后面的元素放到一个新数组中 var [head, ...tail] = names console.log(head, tail) 如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。 let [foo] = 1; let [foo] = false; let [foo] = NaN; let [foo] = undefined; let [foo] = null; let [foo] = {}; // 以上全报错 解构赋值允许指定默认值。 🚨注意:要当数组成员严格等于 undefined ,默认值是才生效的。(null 不严格等于 undefined。) // 解构的默认值 var [itema, itemb, itemc, itemd = "a"] = names console.log(itemd) // 默认值不生效 var [itema, itemb, itemc, itemd = "a"] = ['a', 'b', 'c', null] console.log(itemd) // null // 默认值生效 var [itema, itemb, itemc, itemd = "a"] = ['a', 'b', 'c', undefined] console.log(itemd) // a
对象解构赋值
对对象的解构: { }
对象的属性没有次序,变量必须与属性同名才能取到正确的值
var obj = { name: "a", age: 18, height: 1.88 } // 按顺序取值 var { name, age, height } = obj console.log(name, age, height) // a 18 1.88 // 虽然次序不同,但取值还是一样的 var {age, height, name} = obj console.log(name, age, height) //a 18 1.88 var { age } = obj console.log(age) // 18 如果想改解构后属性的名字,则可以写成下面的形式: var { name: newName } = obj // newName则是属性改后的名字 console.log(newName) 上面的代码中,name 是匹配的模式,newName才是变量。真正被赋值的是变量 newName,而不是模式 name。 对象解构也有默认值 var { address: newAddress = "广州市" } = obj console.log(newAddress)
模板字符串
最近在写算法的时候写出了下面的代码:
😱 太长了,自己竟然写出了一串糟糕的代码,有什么方法可以优化的吗?
模板字符串用法:ES6模板字符串 ` `
, 插值用${ }
if (data == arr[mid]) { j = realMid console.log('+++++ mid = ' + realMid + ' start = ' + realStart + ' end = ' + realEnd + ' j = ' + j + ' +++++'); return '数' + data + '在数组的第' + realMid + '位'; // 使用模板字符串: console.log(`+++++ mid = ${realMid}, start = ${realStart}, end = ${realEnd}, j =${j} +++++`); return `数${data}在数组的第${realMid}位`; }
优化后看起来好多了,下面继续来谈谈模板字符串的其他用法:
多行字符串
在模板字符串中,空格、缩进、换行都会被保留:
// 使用模板字符串前要换行: console.log('String with \n\ multiple \n\ lines') // 使用模板字符串后要换行: console.log(`String with multiple lines`)
嵌入变量
在${ }内部不只是可以写变量,还可以写表达式、函数的调用
const info = `age double is ${age * 2}` console.log(info) function doubleAge() { return age * 2 } const info2 = `double age is ${doubleAge()}`
标签模板字符串
模板字符串可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串
function foo(m, n, x) { console.log(m, n, x, '---------') } const name = "a" const age = 18 foo("Hello", "World", name, age) // Hello World a --------- foo`Hello${name}World${age}` // [ 'Hello', 'World', '' ] a 18 ---------
- 第一个参数依然是模块字符串中整个字符串, 只是被${ }切成多块,放到了一个数组中
- 第二个参数是第一个 ${}里面的内容,第三个参数则是第二个${ }里面的内容
默认参数
JavaScript 函数参数的默认值都是undefined, ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。
传入的参数是undefined时,默认参数才会生效
参数变量是 默认声明 的,所以不能用 let 或 const 再次声明(否则会报错)。
函数参数
function foo(func = () => fn = 'a') { console.log(func()) // a } foo()
对象参数
函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值
function printInfo({ name, age } = { name: "a", age: 18 }) { console.log(name, age) } printInfo({ name: "kobe", age: 40 }) 另外一种写法:先给传入对象一个默认值:{ },再给对象解构赋值的默认值:a和18 function printInfo1({ name = "a", age = 18 } = {}) { console.log(name, age) } printInfo1()
补充注意
- 有默认值的函数的length属性:有默认值的参数不算在length属性里面,且在其之后的参数也不算进length属性里面
function baz(x, y = 30) { console.log(x, y) } console.log(baz.length) // 1
- 使用参数默认值时,函数不能有同名参数(否则会报错)
- 参数作用域
函数声明初始化时,一旦设置了参数默认值,参数会形成一个单独的作用域,等初始化结束后,该作用域即会消失。这种语法在不设置参数默认值的时候是不会出现的。
function foo(m, n = m + 1) { console.log(m, n) //10 11 } foo(10);
上面代码中,调用函数 fn 时,参数形成一个单独的作用域。在这个作用域里面,默认值变量m 指向第一个参数 m
而如果没有参数中没有默认值调用的变量,则会到上层作用域去找:
let m = 20 function foo(n = m + 1) { m = 30 console.log(m, n) //30 21 } foo();
上面代码中,n = m + 1
的 m 是来自全局作用域的m = 20
如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。
剩余参数——rest 操作符
剩余运算符(the rest operator),它的样子看起来和展开操作符一样,但是它是用于解构数组和对象。
rest 操作符可以用于创建有一个变量来接受多个参数的函数。 这些参数被储存在一个可以在函数内部读取的数组中。
用法:rest 操作符语法(...args)
function howMany(...args) { return "You have passed " + args.length + " arguments."; } console.log(howMany(0, 1, 2)); console.log(howMany("string", null, [1, 2, 3], { }));
控制台将显示字符串 You have passed 3 arguments. 和 You have passed 4 arguments.。
使用 rest 参数,就不需要查看 args 数组,并且允许我们在参数数组上使用 map()、filter() 和 reduce()。
剩余参数和arguments的区别
剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;
- arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
- arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供 并且希望以此来替代arguments的;
注意:剩余参数必须放到最后一个位置,否则会报错